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

Блог компании simbirsoft

Перевод C Коварство и Любовь, или Да что вообще может пойти не так?

30.09.2020 08:10:03 | Автор: admin


C позволяет легко выстрелить себе в ногу. На C++ это сделать сложнее, но ногу оторвёт целиком Бьёрн Страуструп, создатель C++.

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



Мы в SimbirSoft тесно сотрудничаем с проектом Secure Code Warrior, обучая других разработчиков создавать безопасные решения. Специально для Хабра мы перевели статью, написанную нашим автором для портала CodeProject.com.

Итак, к коду!


Здесь представлен небольшой фрагмент абстрактного кода на C++. Этот код был специально написан с целью демонстрации всевозможных проблем и уязвимостей, которые потенциально можно встретить на вполне реальных проектах. Как вы можете заметить, это код из Windows DLL (это важный момент). Предположим, что кто-то собирается использовать этот код в некоем (безопасном, разумеется) решении.

Приглядитесь к коду. Что, на ваш взгляд, в нём может пойти не так?

Код
class Finalizer{    struct Data    {        int i = 0;        char* c = nullptr;                union U        {            long double d;                        int i[sizeof(d) / sizeof(int)];                        char c [sizeof(i)];        } u = {};                time_t time;    };        struct DataNew;    DataNew* data2 = nullptr;        typedef DataNew* (*SpawnDataNewFunc)();    SpawnDataNewFunc spawnDataNewFunc = nullptr;        typedef Data* (*Func)();    Func func = nullptr;        Finalizer()    {        func = GetProcAddress(OTHER_LIB, "func")                auto data = func();                auto str = data->c;                memset(str, 0, sizeof(str));                data->u.d = 123456.789;                const int i0 = data->u.i[sizeof(long double) - 1U];                spawnDataNewFunc = GetProcAddress(OTHER_LIB, "SpawnDataNewFunc")        data2 = spawnDataNewFunc();    }        ~Finalizer()    {        auto data = func();                delete[] data2;    }};Finalizer FINALIZER;HMODULE OTHER_LIB;std::vector<int>* INTEGERS;DWORD WINAPI Init(LPVOID lpParam){    OleInitialize(nullptr);        ExitThread(0U);}BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){    static std::vector<std::thread::id> THREADS;        switch (fdwReason)    {        case DLL_PROCESS_ATTACH:            CoInitializeEx(nullptr, COINIT_MULTITHREADED);                        srand(time(nullptr));                        OTHER_LIB = LoadLibrary("B.dll");                        if (OTHER_LIB = nullptr)                return FALSE;                        CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);        break;                case DLL_PROCESS_DETACH:            CoUninitialize();                        OleUninitialize();            {                free(INTEGERS);                                const BOOL result = FreeLibrary(OTHER_LIB);                                if (!result)                    throw new std::runtime_error("Required module was not loaded");                                return result;            }        break;                case DLL_THREAD_ATTACH:            THREADS.push_back(std::this_thread::get_id());        break;                case DLL_THREAD_DETACH:            THREADS.pop_back();        break;    }    return TRUE;}__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw(){    for (int i : integers)        i *= c;        INTEGERS = new std::vector<int>(integers);}int Random(){    return rand() + rand();}__declspec(dllexport) long long int __cdecl _GetInt(int a){    return 100 / a <= 0 ? a : a + 1 + Random();}


Возможно, вы сочли этот код простым, очевидным и достаточно безопасным? Или, может быть, вы нашли в нем некоторые проблемы? А может быть, даже дюжину или две?

Что ж, на самом деле в этом фрагменте более 43 потенциальных угроз различной степени значимости!



На что стоит всё же обратить внимание


1) sizeof(d) (где d это long double) не обязательно кратен sizeof(int)

int i[sizeof(d) / sizeof(int)];

Такая ситуация не проверяется и не обрабатывается здесь. Например, размер long double может быть 10 байт на некоторых платформах (что неверно для компилятора MS VS, но верно для RAD Studio, в прошлом известного как C++ Builder).

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

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



Источник: Medium.com

Между прочим, в отличие от C++, в современном C каламбур типизации полностью допустим (вы же понимаете, что C++ и C разные языки, и вы не должны ожидать, что будете знать C, если вы знаете C++, и наоборот, не так ли?)

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

static_assert(0U == (sizeof(d) % sizeof(int)), Houston, we have a problem);

2) time_t это макрос, в Visual Studio он может ссылаться на 32-битный (старый) или 64-битный (новый) целочисленный тип

time_t time;

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



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

int64_t time;

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

4) проблема с порядком инициализации статических объектов (SIOF): (объект OTHER_LIB в коде используется раньше, чем он был инициализирован)

func = GetProcAddress(OTHER_LIB, "func");

FINALIZER это статический объект, который создается перед вызовом функции DllMain. В его конструкторе мы пытаемся использовать библиотеку, которая еще не загружена. Проблема усугубляется тем, что статический объект OTHER_LIB, который используется статическим объектом FINALIZER, размещается в единице трансляции ниже по коду. Это означает, что инициализирован (обнулен) он также будет позже. Т. е. на момент, когда к нему будут обращаться, он будет содержать некоторый псевдослучайный мусор. WinAPI в целом должен нормально отреагировать на это, потому что с высокой степенью вероятности загруженного модуля с таким дескриптором просто не будет вовсе. И даже если произойдет совершенно невероятное совпадение и он всё таки будет вряд ли в нём будет присутствовать функция по имени Func.

Решение: общий совет избегать использования глобальных объектов, особенно сложных, особенно если они зависят друг от друга, особенно в DLL. Однако, если они все же нужны вам по какой-либо причине, будьте предельно внимательны и осторожны с порядком их инициализации. Чтобы контролировать этот порядок, поместите все экземпляры (определения) глобальных объектов в одну единицу трансляции в нужном порядке, чтобы обеспечить их корректную инициализацию.

5) возвращенный ранее результат не проверяется перед использованием

auto data = func();

func это указатель на функцию. И указывать он должен на функцию из B.dll. Однако, поскольку мы полностью провалили все действия на предыдущем шаге, это будет nullptr. Таким образом, пытаясь разыменовать его, вместо ожидаемого вызова функции мы получим ошибку нарушения прав доступа (access violation) или ошибку защиты памяти (general protection fault) или что-то в этом духе.

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

6) считывание/запись мусора при обмене данными между модулями, скомпилированными с разными alignment/padding настройками

auto str = data->c;

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



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


7) использование размера указателя на массив вместо размера самого массива

memset(str, 0, sizeof(str));

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

Решение:
никогда не путайте sizeof (<полный тип объекта>) и sizeof (<тип указателя на объект> );
не игнорируйте предупреждения компилятора;



вы также можете использовать немного шаблонной магии С++, комбинируя typeid, constexpr и static_assert, чтобы гарантировать правильность типов на этапе компиляции (здесь ещё могут быть полезны type traits, в частности, std::is_pointer).

8) неопределенное поведение при попытке читать иное поле объединения, нежели то, что ранее использовалось для установки значения

9) возможна попытка чтения за пределами допустимой области памяти, если размер long double различается между двоичными модулями

const int i0 = data->u.i[sizeof(long double) - 1U];

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

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


10) даже если B.dll была правильно загружена и функция func правильно экспортирована и импортирована, B.dll все равно уже выгружена из памяти к данному моменту (т. к. ранее была вызвана системная функции FreeLibrary в секции DLL_PROCESS_DETACH функции обратного вызова DllMain)

auto data = func();

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

Решение: внедрить в приложение корректную процедуру финализации, гарантирующую, что все динамические библиотеки завершат свою работу/будут выгружены в правильном порядке. Избегайте использования статических объектов со сложной логикой в DLL. Избегайте выполнения каких-либо операций внутри библиотеки после вызова DllMain/DLL_PROCESS_DETACH (когда библиотека перейдёт к своему последнему этапу жизненного цикла фазе разрушения своих статических объектов).

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


А) Сторонний модуль вызывает LoadLibrary с целью загрузить библиотеку

  • происходит инициализация статических объектов библиотеки (этот этап должен содержать только очень простую логику, вызывается автоматически)
  • происходит вызов DllMain -> DLL_PROCESS_ATTACH (секция должна содержать только очень простую логику, вызывается автоматически)
  • теперь другие потоки приложения могут начать [параллельно] вызывать DllMain -> DLL_THREAD_ATTACH / DLL_THREAD_DETACH (вызывается автоматически, но см. далее примечания в пункте 30).
  • эти секции, возможно, могут содержать некоторую сложную логику (например, индивидуальную инициализацию генератора псевдослучайных чисел для каждого потока), но будьте аккуратны
  • происходит вызов экспортируемой разработчиком библиотеки функции инициализации библиотеки (содержит в себе всю сложную/тяжелую работу по инициализации, вызывается вручную тем, кто загружает библиотеку)
  • непосредственно работа приложения с библиотекой, для чего она (библиотека) и создавалась
  • происходит вызов экспортируемой разработчиком библиотеки функции ДЕинициализации библиотеки (содержит в себе всю сложную/тяжелую работу по ДЕинициализации, вызывается вручную тем, кто выгружает библиотеку)
  • После этой точки избегайте каких-либо действий в библиотеке: все ранее запущенные потоки библиотеки должны быть завершены, прежде чем произойдет возврат из этой функции

В) Другой модуль вызывает FreeLibrary

  • происходит вызов DllMain -> DLL_PROCESS_DETACH (секция должна содержать только очень простую логику, вызывается автоматически)
  • происходит уничтожение статических объектов библиотеки (должен содержать только очень простую логику, вызываемую автоматически)




11) удаление непрозрачного указателя (компилятор должен знать полный тип, чтобы вызвать деструктор, поэтому удаление объекта с помощью opaque pointer может привести к утечке памяти и другим проблемам)

12) если деструктор DataNew является виртуальным, даже при правильном экспорте и импорте класса и получении полной информации о нем, все равно вызов его деструктора на этом этапе является проблемой это, вероятно, приведет к чисто виртуальному вызову функции (так как тип DataNew импортируется из уже выгруженного файла B.dll). Эта проблема возможна, даже если деструктор не является виртуальным.

13) если класс DataNew является абстрактным полиморфным типом, а его базовый класс имеет чистый виртуальный деструктор без тела, в любом случае произойдет чистый вызов виртуальной функции.

14) неопределенное поведение, если выделять память через new и удалять, используя delete[]

delete[] data2;

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

Также хорошей практикой является обнуление указателей на разрушенные объекты.

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



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

Смотрите также

15) ExitThread предпочтительный метод выхода из потока в C. В C++ при вызове этой функции поток завершится до вызова деструкторов локальных объектов (и любой другой автоматической очистки), поэтому завершение потока в C++ следует выполнять просто путем возврата из функции потока

ExitThread(0U);

Решение: никогда не используйте вручную эту функцию в C++ коде.

16) в теле DllMain вызов любых стандартных функций, для которых требуются системные DLL, отличные от Kernel32.dll, может привести к различным трудно диагностируемым проблемам

CoInitializeEx(nullptr, COINIT_MULTITHREADED);

Решение в DllMain:
избегайте любой сложной (де)инициализации
избегайте вызова функций из других библиотек (или, по крайней мере, будьте предельно аккуратны с этим)



17) некорректная инициализация генератора псевдослучайных чисел в многопоточной среде

18) т. к. время, возвращаемое функцией time, имеет разрешение 1 сек., любой поток в программе, который вызывает эту функцию в течение этого периода времени, получит на выходе одинаковое значение. Использование этого числа для инициализации ГПСЧ может привести к возникновению коллизий (например, генерации одинаковых псевдослучайных имён для временных файлов, одинаковых номеров портов и т.д.). Одно из возможных решений смешать (xor) полученный результат с каким-то псевдослучайным значениеv, таким как адрес любого стэка или объекта в куче, более точным временем и т.д.

srand(time(nullptr));

Решение: MS VS требует инициализации ГПСЧ для каждого потока. Кроме того, использование времени Unix в качестве инициализатора предоставляет недостаточно энтропии, предпочтительнее использовать более продвинутую генерацию инициализирующего значения.


19) может вызвать тупик или сбой (или создать циклы зависимости в порядке загрузки DLL)

OTHER_LIB = LoadLibrary("B.dll");

Решение: не используйте LoadLibrary в точке входа DllMain. Любая сложная (де)инициализация должна выполняться в определенных экспортируемых разработчиком DLL функциях, таких как, например, Init и Deint. Библиотека предоставляет эти функции пользователю, а пользователь должен их корректно в нужный момент вызвать. Обе стороны должны строго соблюдать этот контракт.



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

if (OTHER_LIB = nullptr)    return FALSE;

Оператор присваивания путём копирования возвращает ссылку левого типа, т.е. if проверит значение OTHER_LIB (которое будет nullptr) и nullptr будет интерпретировано как false.

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

if/while (<constant> == <variable/expression>)

21) рекомендуется использовать системную функцию _beginthread для создания нового потока в приложении (особенно если приложение было слинковано со статической версией библиотеки времени выполнения C) в противном случае могут возникнуть утечки памяти при вызове ExitThread, DisableThreadLibraryCalls

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

CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);

23) вызов функций COM во время завершения работы DLL может привести к некорректному доступу к памяти, поскольку соответствующий компонент уже может быть выгружен

CoUninitialize();

24) не существует способа контролировать порядок загрузки и выгрузки внутрипроцессных сервисов COM/OLE, поэтому не вызывайте OleInitialize или OleUninitialize из функции DllMain

OleUninitialize();


25) вызов free для блока памяти, выделенного с помощью new

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

free(INTEGERS);

Решение: убедитесь, что старый стиль C ручного выделения памяти не смешан с новым стилем, принятым в C++. Будьте предельно осторожны при управлении ресурсами в функции DllMain.

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

const BOOL result = FreeLibrary(OTHER_LIB);

Решение: не вызывать FreeLibrary в точке входа DllMain.

28) произойдет сбой текущего (возможно, основного) потока

throw new std::runtime_error("Необходимый модуль не был загружен");

Решение: избегайте выбрасывания исключений в функции DllMain. Если DLL не может быть корректно загружена по какой-либо причине, функция должна просто вернуть FALSE. Выбрасывать исключения из секции DLL_PROCESS_DETACH также не следует.

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



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

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


29) можно выкинуть исключение (например, std::bad_alloc), которое здесь не перехватывается

THREADS.push_back(std::this_thread::get_id());

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

Решение: приложите с помощью команды try/catch те инструкции, которые могут выбрасывать исключения, которые вероятнее всего не могут быть обработаны правильно (особенно если они выходят из библиотеки DLL).

Смотрите также

30) UB, если до загрузки этой DLL были представлены потоки

THREADS.pop_back();

Уже существующие на момент загрузки DLL потоки (включая тот, который непосредственно загружает DLL) не вызывают функцию точки входа загружаемой DLL (поэтому они не регистрируются в векторе THREADS во время события DLL_THREAD_ATTACH), в то время как они по-прежнему вызывают его с событием DLL_THREAD_DETACH по завершении.
Это означает, что количество обращений к секциям DLL_THREAD_ATTACH и DLL_THREAD_DETACH функции DllMain будет разным.

31) лучше использовать целочисленные типы фиксированного размера

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

33) доступ к объекту c по его виртуальному адресу (который является общим для модулей) может вызвать проблемы, если указатели по-разному обрабатываются в этих модулях (например, если модули связаны с различными параметрами LARGEADDRESSAWARE)

__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()



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

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



34) внутри функции может быть выброшено исключение:

INTEGERS = new std::vector<int>(integers);

при этом спецификация throw() этой функции пуста:

__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()

std::unexpected вызывается средой выполнения C++ при нарушении спецификации исключения: исключение выбрасывается из функции, спецификация исключения которой запрещает исключения этого типа.

Решение: используйте try / catch (особенно при выделении ресурсов, особенно в DLL) или nothrow форму оператора new. В любом случае, никогда не исходите из наивного предположения о том, что все попытки выделения разного рода ресурсов всегда будут заканчиваться успешно.





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

Проблема 2: возможное переполнение целочисленного типа (что является неопределенным поведением для целочисленных типов со знаком)

return rand() + rand();

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

35) имя экспортируемой функции будет декорировано (изменено), чтобы предотвратить это использование extern C

36) имена, начинающиеся с '_', неявно запрещены для C++, так как этот стиль именования зарезервирован для STL

__declspec(dllexport) long long int __cdecl _GetInt(int a)

Несколько проблем (и их возможные решения):

37) rand не является потокобезопасным, вместо него нужно использовать rand_r/rand_s

38) rand устарел, лучше использовать современный
C++11 <random>

39) не факт, что функция rand была инициализирована конкретно для текущего потока (MS VS требует инициализации этой функции для каждого потока, где она будет вызываться)

40) существуют специальные генераторы псевдослучайных чисел, и в устойчивых ко взлому решениях лучше использовать именно их (подойдут переносимые решения вроде Libsodium/randombytes_buf, OpenSSL/RAND_bytes и т.д.)

41) потенциальное деление на ноль: может вызвать аварийное завершение текущего потока

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

43) потенциальное переполнение целочисленного типа

return 100 / a <= 0 ? a : a + 1 + Random();






И это еще не всё!


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

Наивный подход для решения этой проблемы будет выглядеть примерно так:

bool login(char* const userNameBuf, const size_t userNameBufSize,           char* const pwdBuf, const size_t pwdBufSize) throw(){    if (nullptr == userNameBuf || '\0' == *userNameBuf || nullptr == pwdBuf)        return false;        // Here some actual implementation, which does not checks params    //  nor does it care of the 'userNameBuf' or 'pwdBuf' lifetime,    //   while both of them obviously contains private information     const bool result = doLoginInternall(userNameBuf, pwdBuf);        // We want to minimize the time this private information is stored within the memory    memset(userNameBuf, 0, userNameBufSize);    memset(pwdBuf, 0, pwdBufSize);}

И это, конечно же, не будет работать так, как бы нам хотелось. Что же тогда делать? :(

Неправильное решение 1: если memset не работает, давайте сделаем это вручную!

void clearMemory(char* const memBuf, const size_t memBufSize) throw(){    if (!memBuf || memBufSize < 1U)        return;        for (size_t idx = 0U; idx < memBufSize; ++idx)        memBuf[idx] = '\0';}

Почему и это нам тоже не подходит? Дело в том, что в этом коде нет никаких ограничений, которые бы не позволили современному компилятору оптимизировать его (кстати, функция memset, если она всё же будет использоваться, скорее всего, будет встроенной).



Неправильное решение #2: попытаться улучшить предыдущее решение, поигравшись с ключевым словом volatile

void clearMemory(volatile char* const volatile memBuf, const volatile size_t memBufSize) throw(){    if (!memBuf || memBufSize < 1U)        return;        for (volatile size_t idx = 0U; idx < memBufSize; ++idx)        memBuf[idx] = '\0';        *(volatile char*)memBuf = *(volatile char*)memBuf;    // There is also possibility for someone to remove this "useless" code in the future}

Будет ли это работать? Возможно. Например, такой подход используется в RtlSecureZeroMemory (в чём вы можете убедиться самостоятельно, посмотрев фактическую реализацию этой функции в исходниках Windows SDK).

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

Смотрите также


Неправильное решение #3: использовать неподходящую функцию API ОС (например, RtlZeroMemory) или STL (например, std::fill, std::for_each)

RtlZeroMemory(memBuf, memBufSize);

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

И как же всё-таки правильно??


использовать корректную функцию API ОС, например, RtlSecureZeroMemory для Windows
использовать функцию C11 memset_s:

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

Смотрите также


Подводя итоги


Это, конечно, не полный список всех возможных проблем, нюансов и тонкостей с которыми вы можете столкнуться при написании приложений на C/C++.

Есть также и такие замечательные вещи, как:


И многое другое ;)



Есть что добавить? Поделитесь своими интересным опытом в комментариях!

Подробнее..

Как мы разработали интерактивную веб-схему для зрительных залов

26.06.2020 08:09:31 | Автор: admin
Иногда в приложении надо показать модель помещения допустим, кинотеатра или даже целого стадиона, если вы продаете билеты на концерт Metallica. Если в зале 50-100 тысяч мест, то для их вывода на экран нужно продумать плавный zoom, скроллинг и другие детали. Итак, главный вопрос как показывать тысячи элементов на экране, чтобы это было удобно для пользователей?

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



В одном из проектов мы разработали интерактивную схему зрительного зала с удобным выбором билетов. В основе проекта достаточно стандартный стек технологий: HTML, CSS, JS, библиотека React.

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



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

  1. Плавный zoom и scrolling необходимы для отображения залов с большим количеством мест.
  2. Мини-карта для навигации по залу при максимальном приближении.
  3. Группировка мест по стоимости билетов, с выделением ценовых групп с помощью цвета.
  4. Тултипы для отображения информации о месте или секторе при наведении курсора.
  5. Возможность изменять наименования секторов, мест и рядов при проведении различных мероприятий в одном и том же зале.
  6. Кроссбраузерность и поддержка мобильных устройств.
  7. Гибкая конфигурируемость, которая позволит задать необходимые для конкретного мероприятия параметры, например, настройки увеличения и числа уровней zoom.
  8. Удобный и простой способ встраивания схемы в любые веб-приложения.

Далее расскажем об этапах реализации проекта, о том, какие были технические сложности и как мы их решали.

Выбор технологий


Для разработки приложения мы выбрали библиотеку React, чтобы повысить удобство и скорость разработки благодаря модульности, Virtual DOM и JSX. Для сборки мы взяли Webpack, который позволяет использовать множество различных плагинов для упрощения процесса разработки и поддержки большого числа браузеров.

Для решения задачи рендеринга мы выбирали из двух технологий:

  • SVG
  • Canvas

SVG позволяет создавать элементы любой формы и взаимодействовать с ними, однако, по оценкам заказчика, при количестве узлов более 40 000 значительно падает производительность.

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

Поэтому мы выбрали альтернативный вариант комбинацию SVG и Canvas.Входными данными для нашего комбинированного решения являются квота, список мест, SVG-подложка и объект конфигурации.

  • Квота

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

  • Список мест

Файл JSON для хранения массива мест с их координатами.

  • SVG-подложка

SVG-документ для хранения элементов, которые отвечают за взаимодействие с секторами, и различных декоративных элементов.

  • Объект конфигурации

Файл js для хранения объекта конфигурации (подробнее об этом мы расскажем далее в разделе Гибкая конфигурируемость).

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

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

После этого получаем подобную схему:



Плавный zoom и scrolling


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

Пример работы:

1) До применения zoom.



2) После применения zoom к Canvas. Изображение немного размыто, но пользователь этого почти не замечает.



3) После перерисовки мест (изображение снова чёткое).



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

Пример работы:

1) До начала скроллинга.



2) Пользователь совершил скроллинг вправо, но не отпустил курсор (некоторые места в левой части схемы отсутствуют).



3) Пользователь отпустил курсор (отобразились отсутствующие места).



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

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

  • Мини-карта


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

  • Группировка мест по ценам, с выделением групп с помощью цвета


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

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

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



  • Тултипы


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

Например, ниже представлен тултип места, которое находится в углу схемы:



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

  • Маппинги мест


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

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

  • Кроссбраузерность


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

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

  • Гибкая конфигурируемость


В приложении использован специальный файл javascript для формирования конфигурационного объекта, содержащего все свойства конфигурации, которые мы упоминали ранее (стилизация мест, стилизация тултипов, алгоритм zoom, маппинги, группировка мест), а также множество других свойств.

  • Удобство встраивания


Для легкой интеграции с любыми JS приложениями мы предусмотрели нативную обёртку javascript. Также для решения проблем с кроссдоменностью мы используем iframe. В результате схему можно встраивать в любое веб- или мобильное приложение.

Подводя итоги


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

Спасибо за внимание! Надеемся, что статья была вам полезна.
Подробнее..

Vue.js и слоистая архитектура вынесение бизнес-логики в сервисы

10.05.2021 12:10:00 | Автор: admin

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

В старые времена, когда JQuery только появился, а о фреймворках для серверных языков лишь читали в редких новостях, веб-приложения реализовывали целиком на серверных языках. Зачастую для этого использовали модель MVC (Model-View-Controller): контроллер (controller) принимал запросы, отвечал за бизнес-логику и модели (model) и передавал данные в представление (view), которое рисовало HTML.

Объектно-ориентированное программирование (ООП) на тот момент только начинало формироваться, поэтому разработчики зачастую интуитивно решали, где и какой код надо писать. Таким образом, в мире разработки зародилось такое понятие, как Божественные объекты, которые первоначально отвечали практически за всю работу отдельных частей системы. Например, если в системе была сущность Пользователь, то создавался класс User и в нем писалась вся логика, так или иначе связанная с пользователями. Без разбиения на какие-то ещё файлы. И если приложение было большим, то такой класс мог содержать тысячи строк кода.

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

1. Выход есть

Как известно, Vue.js, React.js и прочие подобные фреймворки основаны на компонентах. То есть, по большому счету, приложение состоит из множества компонентов, которые могут заключать в себе и бизнес-логику и представление и много чего еще. Таким образом, разработчики во многих проектах пишут всю логику в компонентах и эти компоненты, как правило, начинают напоминать те самые божественные классы из прошлого. То есть, если компонент описывает какую-то крупную часть функционала с большим количеством (возможно сложной) логики, то вся эта логика и остается в компоненте. Появляются десятки методов и тысячи строк кода. А если учесть то, что, например, во Vue.js еще есть такие понятия как computed, watch, mounted, created, то логику пишут еще и во все эти части компонента. В итоге, чтобы найти какую-то часть кода, отвечающую за клик по кнопке, надо перелистать десяток экранов js-кода, бегая между methods, computed и прочими частями компонента.

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

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

Вот о таком разбиении кода на слои и пойдет речь, но уже применительно к frontend-фреймворкам, таким как Vue.js, React.js и прочим.

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

2. Создание удобной архитектуры приложения

Рассмотрим пример, в котором вся логика находится в одном компоненте.

2.1. Логика в компоненте

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

methods: {    duplicateCollage (collage) {      this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: true })      dataService.duplicateCollage(collage, false)        .then(duplicate => {          this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: false })        })        .catch(() => {          this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: false })          this.$store.dispatch('errorsSet', { api: `We couldn't duplicate collage. Please, try again later.` })        })    },    deleteCollage (collage, index) {      this.$store.dispatch('updateCollage', { id: collage.id, isDeleting: true })      photosApi.deleteUserCollage(collage)        .then(() => {          this.$store.dispatch('updateCollage', {            id: collage.id,            isDeleting: false,            isDeleted: true          })          this.$store.dispatch('setUserCollages', { total: this.userCollages.total - 1 })          this.$store.dispatch('updateCollage', {            id: collage.id,            deletingTimer: setTimeout(() => {              this.$store.dispatch('updateCollage', { id: collage.id, deletingTimer: null })              this.$store.dispatch('setUserCollages', { items: this.userCollages.items.filter(userCollage => userCollage.id !== collage.id) })               // If there is no one collages left - show templates              if (!this.$store.state.editor.userCollages.total) {                this.currentTabName = this.TAB_TEMPLATES              }            }, 3000)          })        })    },    restoreCollage (collage) {      clearTimeout(collage.deletingTimer)      photosApi.saveUserCollage({ collage: { deleted: false } }, collage.id)        .then(() => {          this.$store.dispatch('updateCollage', {            id: collage.id,            deletingTimer: null,            isDeleted: false          })          this.$store.dispatch('setUserCollages', { total: this.userCollages.total + 1 })        })    }}

2.2. Создание слоя сервисов для бизнес-логики

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

Один из классических способов хоть какого-то разбиения логики это деление на сущности. Например, почти всегда в проекте есть сущность Пользователь или, как в описываемом примере, Коллаж. Таким образом, можно создать папку services и в ней файлы user.js и collage.js. Такие файлы могут быть статическими классами или просто возвращать функции. Главное чтобы вся бизнес-логика, связанная с сущностью, была в этом файле.

services  |_collage.js  |_user.js

В сервис collage.js следует поместить логику дублирования, восстановления и удаления коллажей.

export default class Collage {  static delete (collage) {    // ЛОГИКА УДАЛЕНИЯ КОЛЛАЖА  }   static restore (collage) {    // ЛОГИКА ВОССТАНОВЛЕНИЯ  КОЛЛАЖА  }   static duplicate (collage, changeUrl = true) {    // ЛОГИКА ДУБЛИРОВАНИЯ КОЛЛАЖА  }}

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

Тогда в компоненте надо будет лишь вызвать соответствующие функции сервиса.

methods: {  duplicateCollage (collage) {    CollageService.duplicate(collage, false)  },  deleteCollage (collage) {    CollageService.delete(collage)  },  restoreCollage (collage) {    CollageService.restore(collage)  }}

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

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

import axios from '@/plugins/axios' export default class Api {   static login (email, password) {    return axios.post('auth/login', { email, password })      .then(response => response.data)  }   static logout () {    return axios.post('auth/logout')  }   static getCollages () {    return axios.get('/collages')      .then(response => response.data)  }    static deleteCollage (collage) {    return axios.delete(`/collage/${collage.id}`)      .then(response => response.data)  }    static createCollage (collage) {    return axios.post(`/collage/${collage.id}`)      .then(response => response.data)  }}

3. Что и куда выносить?

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

Бизнес-логика это все то, что описано в требованиях к приложению. Например, ТЗ, документации, дизайны. То есть все то, что напрямую относится к предметной области приложения. Примером может быть метод UserService.login() или ListService.sort(). Для бизнес-логики можно создать сервисный слой с сервисами.

Логика это тот код, который не имеет прямого отношения к предметной области приложения и его бизнес-логике. Например, создание уникальной строки или поиск некоего объекта в массиве. Для логики можно создать слой хэлперов: например, папку helpers и в ней файлы string.js, converter.js и прочие.

Представление все то, что непосредственно связано с компонентом и его шаблоном. Например, изменение реактивных свойств, изменение состояний и прочее. Этот код пишется непосредственно в компонентах (methods, computed, watch и так далее).

login (email, password) {  this.isLoading = true  userService.login(email, password)    .then(user => {      this.user = user      this.isLoading = false    })}

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

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

4. От простого к сложному

В идеале можно сделать архитектуру на ООП, в которой будут, помимо сервисов, еще и модели. Это классы, описывающие сущности приложения. Те же User или Collage. Но использоваться они будут вместо обычных объектов данных.

Рассмотрим список пользователей.

Классический способ вывода ФИО пользователей выглядит так.

<template><div class="users">  <div    v-for="user in users"    class="user"  >    {{ getUserFio(user) }}  </div></div></template> <script>import axios from '@/plugins/axios' export default {  data () {    return {      users: []    }  },  mounted () {    this.getList()  },  methods: {    getList() {      axios.get('/users')        .then(response => this.users = response.data)    },    getUserFio (user) {      return `${user.last_name} ${user.first_name} ${user.third_name}`    }  }}</script>

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

Для начала следует создать модель Пользователь.

export default class User {  constructor (data = {}) {    this.firstName = data.first_name    this.secondName = data.second_name    this.thirdName = data.third_name  }   getFio () {    return `${this.firstName} ${this.secondName} ${this.thirdName}`  }}

Далее следует импортировать эту модель в компонент.

import UserModel from '@/models/user'

С помощью сервиса получить список пользователей и преобразовать каждый объект в массиве в объект класса (модели) User.

methods: {   getList() {     const users = userService.getList()     users.forEach(user => {       this.users.push(new UserModel(user))     })   },

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

<template><div class="users">  <div    v-for="user in users"    class="user"  >    {{ user.getFio() }}  </div></div></template>

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

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

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

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

Спасибо за внимание! Будем рады ответить на ваши вопросы.

Подробнее..

2d-графика в React с three.js

06.06.2021 12:22:20 | Автор: admin

У каждого из вас может возникнуть потребность поработать с графикой при создании React-приложения. Или вам нужно будет отрендерить большое количество элементов, причем сделать это качественно и добиться высокой производительности при перерисовке элементов. Это может быть анимация либо какой-то интерактивный компонент. Естественно, первое, что приходит в голову это Canvas. Но тут возникает вопрос: Какой контекст использовать?. У нас есть выбор 2d-контекст либо WebGl. А как на счёт 2d-графики? Тут уже не всё так очевидно.

При работе над задачами с высокой производительностью мы попробовали оба решения, чтобы на практике определиться, какой из двух контекстов будет эффективнее. Как и ожидалось, WebGl победил 2d-контекст, поэтому кажется, что выбор прост.

Но тут возникает проблема. Вы сможете ощутить её, если начнете работать с документацией WebGl. С первых мгновений становится понятно, что она слишком низкоуровневая, в отличие от 2d context. Поэтому, чтобы не писать тонны кода, перед нами встаёт очевидное решение использование библиотеки. Для реализации этой задачи подходят библиотеки pixi.js и three.js с качественной документацией, большим количеством примеров и крупным комьюнити разработчиков.

Pixi.js или three.js

На первый взгляд, выбрать подходящий инструмент несложно: используем pixi.j для 2d-графиков, а three.js для 3d. Однако, чем 2d отличается от 3d? По сути дела, отсутствием 3d-перспективы и фиксированным значением по третьей координате. Для того чтобы не было перспективы, мы можем использовать ортографическую камеру.

Вероятно, вы спросите: Что за камера?. Camera это одно из ключевых понятий при реализации графики, наряду со scene и renderer. Для наглядности приведу аналогию. Представим, что вы стоите в комнате, держите в руках смартфон и снимаете видеоролик. Та комната, в которой вы снимаете видео это scene. В комнате могут быть различные предметы, например, стол и стулья это объекты на scene. В роли camera выступает камера смартфона, в роли renderer матрица смартфона, которая проецирует 3d-комнату на 2d-экран.

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

Таким образом, three.js также подходит для 2d-графики. Так что же в итоге выбрать? Мы попробовали оба варианта и выявили на практике несколько преимуществ three.js.

  • Во-первых, нам нужно было выполнить интерактивное взаимодействие с элементами на сцене. Написать собственную реализацию достаточно трудозатратно, но в обеих библиотеках уже есть готовые решения: в pixi.js из коробки, в three.js библиотека three.interaction.

Казалось бы, в этом плане библиотеки равноценны, но это лишь первое впечатление. Особенность реализации интерактивности в pixi.js предполагает, что интерактивные элементы должны иметь заливку. Но как быть с линейными графиками? У них же нет заливки. Без собственного решения в этом случае не обойтись. Что же касается three.js, то тут этой проблемы нет, и линейные графики также интерактивны.

  • Еще одна задача это экспорт в SVG. Нам нужно было реализовать функциональность, которая позволит экспортировать в SVG то, что мы видим на сцене, чтобы потом это изображение можно было использовать в печати. В three.js для этого есть готовый пример, а вот в pixi.js нет.

  • Ну и будем честны с собой, в three.js больше примеров реализации тех или иных задач. К тому же, изучив эту библиотеку, при желании мы можем работать с 3d-графикой, а вот в случае pixi.js такого преимущества у нас нет.

Исходя из всего вышеописанного, наш выбор очевиден это three.js.

Three.js и React

После выбора библиотеки мы сталкиваемся с новой дилеммой использовать react-обертку или каноническую three.js.

Для react есть реализация обёртки это react-three-fiber. На первый взгляд, в ней довольно мало документации, что может показаться проблемой. Действительно, при переносе кода из примеров three.js в react-three-fiber возникает много вопросов по синтаксису.

Однако, на практике все не так уж сложно. У этой библиотеки есть обёртка drei с неплохим storybook с готовой реализацией множества различных примеров. Впрочем, всё, что за находится пределами этой реализации, по-прежнему может причинять боль.

Еще одна проблема это жёсткая привязка к react. А если мы отлично реализуем view с графикой и захотим использовать где-то ещё? В таком случае снова придётся поработать.

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

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

Создаём класс three.js для работы с библиотекой three.js. По сути, всё взаимодействие с ней будет проходить в объекте данного класса.

class Three {  constructor({    canvasContainer,    sceneSizes,    rectSizes,    color,    colorChangeHandler,  }) {    // Для использования внутри класса добавляем параметры к this    this.sceneSizes = sceneSizes;    this.colorChangeHandler = colorChangeHandler;     this.initRenderer(canvasContainer); // создание рендерера    this.initScene(); // создание сцены    this.initCamera(); // создание камеры    this.initInteraction(); // подключаем библиотеку для интерактивности    this.renderRect(rectSizes, color); // Добавляем квадрат на сцену    this.render(); // Запускаем рендеринг  }   initRenderer(canvasContainer) {    // Создаём редерер (по умолчанию будет использован WebGL2)    // antialias отвечает за сглаживание объектов    this.renderer = new THREE.WebGLRenderer({antialias: true});     //Задаём размеры рендерера    this.renderer.setSize(this.sceneSizes.width, this.sceneSizes.height);     //Добавляем рендерер в узел-контейнер, который мы прокинули извне    canvasContainer.appendChild(this.renderer.domElement);  }   initScene() {    // Создаём объект сцены    this.scene = new THREE.Scene();     // Задаём цвет фона    this.scene.background = new THREE.Color("white");  }   initCamera() {    // Создаём ортографическую камеру (Идеально подходит для 2d)    this.camera = new THREE.OrthographicCamera(      this.sceneSizes.width / -2, // Левая граница камеры      this.sceneSizes.width / 2, // Правая граница камеры      this.sceneSizes.height / 2, // Верхняя граница камеры      this.sceneSizes.height / -2, // Нижняя граница камеры      100, // Ближняя граница      -100 // Дальняя граница    );     // Позиционируем камеру в пространстве    this.camera.position.set(      this.sceneSizes.width / 2, // Позиция по x      this.sceneSizes.height / -2, // Позиция по y      1 // Позиция по z    );  }   initInteraction() {    // Добавляем интерактивность (можно будет навешивать обработчики событий)    new Interaction(this.renderer, this.scene, this.camera);  }   render() {    // Выполняем рендеринг сцены (нужно запускать для отображения изменений)    this.renderer.render(this.scene, this.camera);  }   renderRect({width, height}, color) {    // Создаём геометрию - квадрат с высотой "height" и шириной "width"    const geometry = new THREE.PlaneGeometry(width, height);     // Создаём материал с цветом "color"    const material = new THREE.MeshBasicMaterial({color});     // Создаём сетку - квадрат    this.rect = new THREE.Mesh(geometry, material);     //Позиционируем квадрат в пространстве    this.rect.position.x = this.sceneSizes.width / 2;    this.rect.position.y = -this.sceneSizes.height / 2;     // Благодаря подключению "three.interaction"    // мы можем навесить обработчик нажатия на квадрат    this.rect.on("click", () => {      // Меняем цвет квадрата      this.colorChangeHandler();    });     this.scene.add(this.rect);  }   // Служит для изменения цвета квадрат  rectColorChange(color) {    // Меняем цвет квадрата    this.rect.material.color.set(color);     // Запускаем рендеринг (отобразится квадрат с новым цветом)    this.render();  }}

А теперь создаём класс ThreeContauner, который будет React-обёрткой для нативного класса Three.

import {useRef, useEffect, useState} from "react"; import Three from "./Three"; // Размеры сцены и квадратаconst sceneSizes = {width: 800, height: 500};const rectSizes = {width: 200, height: 200}; const ThreeContainer = () => {  const threeRef = useRef(); // Используется для обращения к контейнеру для canvas  const three = useRef(); // Служит для определения, создан ли объект, чтобы не создавать повторный  const [color, colorChange] = useState("blue"); // Состояние отвечает за цвет квадрата   // Handler служит для того, чтобы изменить цвет  const colorChangeHandler = () => {    // Просто поочерёдно меняем цвет с серого на синий и с синего на серый    colorChange((prevColor) => (prevColor === "grey" ? "blue" : "grey"));  };   // Создание объекта класса Three, предназначенного для работы с three.js  useEffect(() => {    // Если объект класса "Three" ещё не создан, то попадаем внутрь    if (!three.current) {      // Создание объекта класса "Three", который будет использован для работы с three.js      three.current = new Three({        color,        rectSizes,        sceneSizes,        colorChangeHandler,        canvasContainer: threeRef.current,      });    }  }, [color]);   // при смене цвета вызывается метод объекта класса Three  useEffect(() => {    if (three.current) {      // Запускаем метод, который изменяет в цвет квадрата      three.current.rectColorChange(color);    }  }, [color]);   // Данный узел будет контейнером для canvas (который создаст three.js)  return <div className="container" ref={threeRef} />;}; export default ThreeContainer;

А вот пример работы данного приложения.

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

После нажатия на квадрат он меняет цвет и становится белым.

Как мы видим, использование нативного three.js внутри React-приложения не вызывает каких-либо проблем, и этот подход достаточно удобен. Однако, на плечи разработчика в этом случае ложится нагрузка, связанная с добавлением/удалением узлов со сцены. Таким образом, теряется тот подход, который берёт на себя virtual dom внутри React-приложения. Если вы не готовы с этим мириться, обратите внимание на библиотеку react-three-fiber в связке с библиотекой drei этот способ позволяет мыслить в контексте React-приложения.

Рассмотрим реализованный выше пример с использованием этих библиотек:

import {useState} from "react";import {Canvas} from "@react-three/fiber";import {Plane, OrthographicCamera} from "@react-three/drei"; // Размеры сцены и квадратаconst sceneSizes = {width: 800, height: 500};const rectSizes = {width: 200, height: 200}; const ThreeDrei = () => {  const [color, colorChange] = useState("blue"); // Состояние отвечает за цвет квадрата   // Handler служит для того, чтобы  const colorChangeHandler = () => {    // Просто поочерёдно меняем цвет с серого на синий и с синего на белый    colorChange((prevColor) => (prevColor === "white" ? "blue" : "white"));  };   return (    <div className="container">      {/* Здесь задаются параметры, которые отвечают за стилизацию сцены */}      <Canvas className="container" style={{...sceneSizes, background: "grey"}}>        {/* Камера задаётся по аналогии с нативной three.js, но нужно задать параметр makeDefault,         чтобы применить именно её, а не камеру заданную по умолчанию */}        <OrthographicCamera makeDefault position={[0, 0, 1]} />        <Plane          // Обработка событий тут из коробки          onClick={colorChangeHandler}          // Аргументы те же и в том же порядке, как и в нативной three.js          args={[rectSizes.width, rectSizes.height]}        >          {/* Материал задаётся по аналогии с нативной three.js,               но нужно использовать attach для указания типа прикрепления узла*/}          <meshBasicMaterial attach="material" color={color} />        </Plane>      </Canvas>    </div>  );}; export default ThreeDrei;

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

В этой статье мы с вами рассмотрели два подхода в использовании библиотеки three.js внутри React-приложения. Каждый из этих подходов имеет свои плюсы и минусы, поэтому выбор за вами.

Спасибо за внимание! Надеемся, что наш опыт был для вас полезен.

Подробнее..

Как безболезненно мигрировать с RxJava на Kotlin CoroutinesFlow

11.01.2021 10:24:00 | Автор: admin
Для выполнения асинхронных операций в Android-приложениях, где нужна загрузка и обработка любых данных, долгое время использовали RxJava и о том, как перейти на RxJava 3, мы уже писали в нашем блоге. Сейчас на смену фреймворку постепенно приходят инструменты Kotlin Coroutines+Flow. Актуальность этой связки подтверждается тем, что Google сделал Kotlin приоритетным языком для Android-разработки.

Корутины позволяют тратить меньше системных ресурсов, чем RxJava. Кроме того, поскольку они являются частью Kotlin, Android предоставляет удобные инструменты для работы с ними например, viewModelScope и lifecycleScope. В этой статье мы рассмотрим use cases, распространенные в Rx Java, и то, какие возможности вы получите при переходе на Flow.



Переключение потоков и создание


Для начала сравним, как происходит переключение потоков в RxJava и Flow.

RxJava


Observable.create<Int> { emitter ->emitter.onNext(1)emitter.onNext(2)emitter.onNext(3)emitter.onComplete()}.observeOn(Schedulers.io()).map {printThread(map1 value = $it)it + it}.doOnNext { printThread(after map1 -> $it) }.observeOn(Schedulers.computation()).map {printThread(map2 value = $it)it * it}.doOnNext { printThread(after map2 -> $it) }.observeOn(Schedulers.single()).subscribe ({printThread(On Next $it)},{printThread(On Error)},{printThread(On Complete)})


При этом сложение выполняется в IO шедулере, умножение в computation шедулере, а подписка в single.

Flow


Повторим этот же пример для Flow:

launch {flow {emit(1)emit(2)emit(3)}.map {printThread(map1 value = $it)it + it}.onEach { printThread(after map1 -> $it) }.flowOn(Dispatchers.IO).map {printThread(map2 value = $it)it * it}.onEach { printThread(after map2 -> $it) }.flowOn(Dispatchers.Default).onCompletion { printThread(onCompletion) }.collect { printThread(received value $it) }}


В результате можно отметить следующее:

1) observeOn переключает поток, в котором будут выполняться последующие операторы, а flowOn определяет диспетчер выполнения для предыдущих операторов.

2) Метод collect() будет выполняться в том же диспетчере, что и launch, а emit данных будет происходить в Dispatchers.IO. Метод subscribe() будет выполняться в Schedulers.single(), потому что идет после него.

3) Flow также имеет стандартные методы создания flow:

  • flowOf(): в примере можно было бы использовать Observable.fromArray(1, 2, 3) и flowOf(1, 2, 3)
  • extenstion function asFlow(), который превращает Iterable, Sequence, массивы во flow
  • билдер flow { }

4) В данном примере Flow, как и RxJava, представляет собой cold stream данных: до вызова методов collect() и subscribe() никакой обработки происходить не будет.

5) В RxJava нужно явно вызывать emitter.onComplete(). В Flow метод onCompletion() будет автоматически вызываться после окончания блока flow { }.

6) При попытке сделать эмит данных из другого диспетчера, с помощью withContext, например, приведет к ошибке.

Exception in thread main java.lang.IllegalStateException: Flow invariant is violated:
Flow was collected in [BlockingCoroutine{Active}@5df83c81, BlockingEventLoop@3383bcd],
but emission happened in [DispatchedCoroutine{Active}@7fbc37eb, Dispatchers.IO].
Please refer to 'flow' documentation or use 'flowOn' instead

Подписка и отписка на источник данных


В RxJava метод Observable.subscribe() возвращает объект Disposable. Он служит для отписки от источника данных, когда новые порции данных от текущего источника уже не нужны. Важно иметь доступ к этому объекту, чтобы вовремя отписываться и избегать утечек.

Для Flow ситуация схожа: так как метод collect() suspend метод, он может быть запущен только внутри корутины. Следовательно, отписка от flow происходит в момент отмены Job корутины:

val job = scope.launch {flow.collect { }}job.cancel() // тут произойдет отписка от flow

В случае же использования viewModelScope об этом заботиться не нужно: все корутины, запущенные в рамках этого scope, будут отменены, когда ViewModel будет очищена, т.е. вызовется метод ViewModel.onCleared(). Для lifecycleScope ситуация аналогична: запущенные в его рамках корутины будут отменены, когда соответствующий Lifecycle будет уничтожен.

Обработка ошибок


В RxJava есть метод onError(), который будет вызван в случае возникновения какой-либо ошибки и на вход получит данные о ней. В Flow тоже есть такой метод, он называется catch(). Рассмотрим следующий пример.

RxJava


Observable.fromArray(1, 2, 3).map {val divider = Random.Default.nextInt(0, 1)it / divider}.subscribe({ value ->println(value)},{ e ->println(e)})


При возникновении ArithmeticException будет срабатывать onError(), и информация об ошибке будет напечатана в консоль.

Flow


flowOf(1, 2, 3).map {val divider = Random.Default.nextInt(0, 1)it / divider}.catch { e -> println(e) }.collect { println(it) }


Этот же пример, переписанный на flow, можно представить с помощью catch { }, который под капотом имеет вид привычной конструкции try/catch.

Операторы RxJava onErrorResumeNext и onErrorReturn можно представить в виде:

catch { emit(defaultValue) } // onErrorReturn

catch { emitAll(fallbackFlow) } // onErrorResumeNext

В Flow, как и в RxJava, есть операторы retry и retryWhen, позволяющие повторить операции в случае возникновения ошибки.

Операторы


Рассмотрим наиболее распространенные операторы RxJava и найдем их аналоги из Flow.



Подробнее с операторами Flow можно познакомиться здесь.

Некоторые операторы Flow (например, merge) помечены как экспериментальные или отсутствующие. Их api может измениться (как, например, для flatMapMerge), или их могут задепрекейтить, то есть они станут недоступны. Это важно помнить при работе с Flow. При этом отсутствие некоторых операторов компенсируется тем, что flow всегда можно собрать в список и работать уже с ним. В стандартной библиотеке Kotlin есть множество функций для работы со списками.

Также у Flow отсутствуют отдельные операторы троттлинга и другие операторы, которые работают с временными промежутками. Это можно объяснить молодостью библиотеки, а также тем, что, согласно словам разработчика Kotlin Романа Елизарова, команда Jetbrains не планирует раздувать библиотеку множеством операторов, оставляя разработчикам возможность компоновать нужные операторы самостоятельно, предоставляя им удобные блоки для сборки.

Backpressure


Backpressure это ситуация, когда производитель данных выдает элементы подписчику быстрее, чем тот их может обработать. Готовые данные, в ожидании того, как подписчик сможет их обработать, складываются в буфер Observable. Проблема такого подхода в том, что буфер может переполниться, вызвав OutOfMemoryError.

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

После появления в RxJava 2 Flowable произошло разделение на источники данных с поддержкой backpressure (Flowable) и Observable, которые теперь не поддерживают backpressure. При работе с RxJava требуется правильно выбрать тип источника данных для корректной работы с ним.

У Flow backpressure заложена в Kotlin suspending functions. Если сборщик flow не может принимать новые данные в настоящий момент, он приостанавливает источник. Возобновление происходит позднее, когда сборщик flow снова сможет получать данные. Таким образом, в Kotlin нет необходимости выбирать тип источника данных, в отличие от RxJava.

Hot streams


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

Горячие источники данных полезны, например, при подписке на события от View: при этом нужно получать только новые события, нет смысла обрабатывать заново все пользовательские действия. Также мы не можем запретить пользователю нажимать на экран до тех пор, пока мы не будем готовы обрабатывать его действия. Для обработки событий от View в реактивном виде существует библиотека RxBinding, которая имеет поддержку RxJava3.

В Kotlin Flow есть свои возможности для работы с горячим flow, который производит данные вне зависимости от наличия подписчиков и выдает новые данные одновременно всем имеющимся подписчикам. Для этого можно использовать Channel, SharedFlow, чтобы отправлять новые порции данных одновременно всем подписанным сборщикам.

Кстати, для Flow тоже есть отличная библиотека для обработки событий от View Corbind. В ней есть поддержка большинства Android-виджетов.

RxJava Subjects


Subject в RxJava это специальный элемент, который одновременно является источником данных и подписчиком. Он может подписаться на один или несколько источников данных, получать от них порции данных и отдавать их своим подписчикам.

Аналог Subject в Flow это Channel, в частности, BroadcastChannel. Существуют различные варианты их реализации: с буферизацией данных (ArrayBroadcastChannel), с хранением только последнего элемента (ConflatedBroadcastChannel). Но важно помнить, что, так как библиотека Kotlin Flow молода и постоянно развивается, ее части могут меняться. Так получилось и в случае с BroadcastChannel: в своей статье Роман Елизаров сообщил, что, начиная с версии 1.4 будет предложено лучшее решение shared flows, а BroadcastChannel ждет deprecation в ближайшем будущем.

Заключение


В данной статье мы сравнили RxJava и Kotlin Flow, рассмотрели их схожие моменты и аналоги частей RxJava в Flow. При этом Flow хорошо подойдет в качестве инструмента для обработки событий в реактивном стиле в проектах на Kotlin, использующих паттерн MVVM: благодаря viewModelScope и lifecycleScope запускать корутины можно быстро и удобно, не боясь утечек. В связи с тем, что популярность Kotlin и его инструментов растет, а также этот язык является приоритетным для разработки Android-приложений, в ближайшие годы связка Coroutines+Flow может заменить RxJava скорее всего, новые проекты будут написаны именно с помощью нее. На первый взгляд, миграция с RxJava на Flow не представляется болезненной, потому что в обоих случаях есть похожие операторы и разделение общей концепции Reactive streams. Кроме того, Kotlin имеет достаточно большое комьюнити, которое постоянно развивается и помогает разработчикам в изучении новых возможностей.

А вы готовы мигрировать на корутины? Приглашаем поделиться мнениями!
Подробнее..

Перевод Разработка на Android как найти подходящую абстракцию для работы со строками

15.02.2021 12:06:03 | Автор: admin

В своих проектах мы стараемся по мере необходимости покрывать код тестами и придерживаться принципов SOLID и чистой архитектуры. Хотим поделиться с читателями Хабра переводом статьи Hannes Dorfmann автора серии публикаций об Android-разработке. В этой статье описан способ, который помогает абстрагировать работу со строками, чтобы скрыть детали взаимодействия с разными типами строковых ресурсов и облегчить написание юнит-тестов.

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

Фото: UnsplashФото: Unsplash

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

Уровень абстракции для строк?

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

  • Простой строковый ресурс вроде R.string.some_text, отображаемый на экране с помощью resources.getString(R.string.some_text)

  • Отформатированная строка, которая форматируется во время выполнения, т.е. context.getString(R.string.some_text, arg1, 123) с

<string name=some_formatted_text>Some formatted Text with args %s %i</string>
  • Более сложные строковые ресурсы, такие как Plurals, которые перегружены, например resources.getQuantityString(R.plurals.number_of_items, 2):

<plurals name="number_of_items">  <item quantity="one">%d item</item>  <item quantity="other">%d items</item></plurals>
  • Простой текст, который не загружается из ресурсов Android в XML-файле вроде strings.xml, а уже загружен в переменную типа String и не требует дальнейшего преобразования (в отличие от R.string.some_text). Например, фрагмент текста, извлеченный из json ответа с сервера.

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

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

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

Давайте шаг за шагом рассмотрим эти моменты на конкретном примере: предположим, мы хотим загружать строку с сервера по http, и если это не удается, мы отображаем аварийную fallback-строку из strings.xml. Например, так:

class MyViewModel(  private val backend : Backend,  private val resources : Resources // ресурсы Android из context.getResources()) : ViewModel() {  val textToDisplay : MutableLiveData<String>  // MutableLiveData используется для удобства чтения   fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = text    } catch (t : Throwable) {      textToDisplay.value = resources.getString(R.string.fallback_text)    }  }}

Детали реализации просочились в нашу MyViewModel, что в целом усложняет ее тестирование. Действительно, чтобы написать тест для loadText(), нам надо либо замокать Resources, либо ввести интерфейс наподобие StringRepository (по шаблону "репозиторий"), чтобы при тестировании мы могли заменить его другой реализацией:

interface StringRepository{  fun getString(@StringRes id : Int) : String} class AndroidStringRepository(  private val resources : Resources // ресурсы Android из context.getResources()) : StringRepository {  override fun getString(@StringRes id : Int) : String = resources.getString(id)} class TestDoubleStringRepository{    override fun getString(@StringRes id : Int) : String = "some string"}

Затем вью-модель получит StringRepository вместо непосредственно ресурсов, и в этом случае все будет в порядке, не так ли?

class MyViewModel(  private val backend : Backend,  private val stringRepo : StringRepository // детали реализации скрываются за интерфейсом) : ViewModel() {  val textToDisplay : MutableLiveData<String>     fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = text    } catch (t : Throwable) {      textToDisplay.value = stringRepo.getString(R.string.fallback_text)    }  }}

На эту вью-модель можно написать такой юнит-тест:

@Testfun when_backend_fails_fallback_string_is_displayed(){  val stringRepo = TestDoubleStringRepository()  val backend = TestDoubleBackend()  backend.failWhenLoadingText = true // заставит backend.getText() выкинуть исключение  val viewModel = MyViewModel(backend, stringRepo)  viewModel.loadText()   Assert.equals("some string", viewModel.textToDisplay.value)}

С введением interface StringRepository мы добавили уровень абстракции и решили задачу, верно? Нет. Мы добавили уровень абстракции, но реальная проблема все еще перед нами:

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

  • Кроме того, если рассматривать реализацию TestDoubleStringRepository и тест, который мы написали, насколько он является значимым? TestDoubleStringRepository всегда возвращает одну и ту же строку. Мы могли бы совершенно испортить код вью-модели, передавая R.string.foo вместо R.string.fallback_text в StringRepository.getString(), и наш тест все равно бы был пройден. Конечно, можно улучшить TestDoubleStringRepository, чтобы он не просто всегда возвращал одну и ту же строку:

class TestDoubleStringRepository{    override fun getString(@StringRes id : Int) : String = when(id){      R.string.fallback_test -> "some string"      R.string.foo -> "foo"      else -> UnsupportedStringResourceException()    }}

Но насколько это поддерживаемо? Вы хотели бы так делать для всех строк в вашем приложении (если их у вас сотни)?

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

Нам поможет TextResource

Придуманная нами абстракция называется TextResource. Это модель для представления текста, которая относится к слою domain. Таким образом, это объект первого класса в нашей бизнес-логике. И выглядит это следующим образом:

sealed class TextResource {  companion object { // Используется для статических фабричных методов, чтобы файл с конкретной реализацией оставался приватным    fun fromText(text : String) : TextResource = SimpleTextResource(text)    fun fromStringId(@StringRes id : Int) : TextResource = IdTextResource(id)    fun fromPlural(@PluralRes id: Int, pluralValue : Int) : TextResource = PluralTextResource(id, pluralValue)  }} private data class SimpleTextResource( // Можно будет также использовать inline классы  val text : String) : TextResource() private data class IdTextResource(  @StringRes id : Int) : TextResource() private data class PluralTextResource(    @PluralsRes val pluralId: Int,    val quantity: Int) : TextResource() // можно будет добавить и другие виды текста...

Так выглядит вью-модель с TextResource:

class MyViewModel(  private val backend : Backend // Обратите, пожалуйста, внимание, что не надо передавать ни какие-то ресурсы, ни StringRepository.) : ViewModel() {  val textToDisplay : MutableLiveData<TextResource> // Тип уже не String     fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = TextResource.fromText(text)    } catch (t : Throwable) {      textToDisplay.value = TextResource.fromStringId(R.string.fallback_text)    }  }}

Основные отличия:

1) textToDisplay поменялся c LiveData<String> на LiveData<TextResource>, поэтому теперь вью-модели не нужно знать, как переводить разные типы текста в String. Она должна уметь переводить их в TextResource. Однако, это нормально, как будет видно далее, TextResource это абстракция, которая решит наши проблемы.

2) Посмотрите на конструктор вью-модели. Нам удалось удалить неправильную абстракцию StringRepository (при этом нам не нужны Resources). Вас, возможно, интересует, как теперь писать тесты? Так же просто, как напрямую протестировать TextResource. Дело в том, что эта абстракция также абстрагирует зависимости Android, такие как ресурсы или контекст (R.string.fallback_text это просто Int). И вот как выглядит наш юнит-тест:

@Testfun when_backend_fails_fallback_string_is_displayed(){  val backend = TestDoubleBackend()  backend.failWhenLoadingText = true // заставит backend.getText() выкинуть исключение  val viewModel = MyViewModel(backend)  viewModel.loadText()   val expectedText = TextResource.fromStringId(R.string.fallback_text)  Assert.equals(expectedText, viewModel.textToDisplay.value)  // для data class-ов генерируются методы equals, поэтому мы легко можем их сравнивать}

Пока все хорошо, но не хватает одной детали: как нам преобразовать TextResource в String, чтобы можно было отобразить его, например, в TextView? Что ж, это касается исключительно отрисовки в Android, и мы можем создать функцию расширения и заключить ее в слое UI.

// Можно получить ресурсы с помощью context.getResources()fun TextResource.asString(resources : Resources) : String = when (this) {   is SimpleTextResource -> this.text // smart cast  is IdTextResource -> resources.getString(this.id) // smart cast  is PluralTextResource -> resources.getQuantityString(this.pluralId, this.quantity) // smart cast}

А поскольку преобразование TextResource в String происходит в UI (на уровне представления) архитектуры нашего приложения, TextResource будет переводиться при изменении конфигурации (т.е. при изменении системного языка на смартфоне), что обеспечит правильную локализацию строки для любых ресурсов R.string.* вашего приложения.

Бонус: вы можете легко написать юнит-тест для TextResource.asString(), создавая моки для ресурсов. При этом не следует создавать мок для каждого отдельного строкового ресурса в приложении, потому что на самом деле нужно протестировать всего лишь работу конструкции when. Поэтому здесь будет корректно всегда возвращать одну и ту же строку из замоканного resources.getString(). Кроме того, TextResource можно многократно использовать в коде, и он соответствует принципу открытости/закрытости. Так, его можно расширить для будущих вариантов использования, добавив всего несколько строк кода: новый класс данных, который расширяет TextResource, и новую ветку в конструкцию when в TextResource.asString().

Поправка: как правильно подметили в комментариях, TextResource не следует принципу открытости/закрытости. Можно было бы поддержать принцип открытости/закрытости для TextResource, если бы у sealed class TextResouce была abstract fun asString(r: Resources), которую реализуют все подклассы. Я лично считаю, что можно пожертвовать принципом открытости/закрытости в пользу упрощения структур данных и работать с расширенной функцией asString(r: Resources), которая находится за пределами иерархии наследования (именно этот способ описан в статье и является достаточно расширяемым, хотя и не настолько, как с принципом открытости/закрытости). Почему? Я считаю, что добавление функции с параметром Resources к публичному API TextResource проблематично, потому что только часть подклассов нуждается в этом параметре (например, SimpleTextResource такого вообще не требует). Кроме того, если такая реализация станет частью общедоступного API, это может привести к увеличению накладных расходов на поддержку кода, а также к появлению дополнительных сложностей (особенно при тестировании).

Выводы

Описанный в статье способ можно применять для работы со строками и другими ресурсами: dimens, изображениями, цветами. При этом в каждом случае важно анализировать, насколько уместна работа с абстракциями. Хотя злоупотреблять ими нежелательно, в некоторых случаях абстракции могут быть полезны в том числе, как мы уже упоминали, для более простого написания тестов. Ждем ваших отзывов об этом методе и его практическом применении!

Подробнее..

Интеграция интернет-магазина на 1С-Битрикс с Mindbox

17.07.2020 10:14:14 | Автор: admin
Для развития систем лояльности интернет-магазины обращаются к платформам автоматизации маркетинга, Customer Data Platform (CDP). При этом иногда для успешной интеграции нужно сохранять больше данных, чем указано в документации к API.

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



С помощью сервисов Customer Data Platform ритейлеры узнают портрет своего покупателя, в том числе поведенческие данные. Эта информация хранится в CDP в защищенном виде и помогает ритейлерам в проведении маркетинговых кампаний и аналитике.

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

Один из наших клиентов сеть магазинов электроники принял решение подключиться к CDP-платформе Mindbox и обратился к нам за помощью в интеграции. Мы проводили интеграцию по ключевым пользовательским сценариям: авторизация, добавление в корзину, оплата и др.

Предыстория


Интернет-магазины могут подключиться к Mindbox двумя основными способами: с помощью API либо JavaScript SDK (об отличиях мы расскажем далее).

Для выбора оптимального способа мы обратились к документации Mindbox, а если информации не хватало, то задавали вопросы менеджеру. Мы выяснили, что наше сотрудничество совпало с периодом бурного роста платформы Mindbox: среднесуточная нагрузка по вызовам API Mindbox увеличилась вдвое (до 120 тысяч запросов в минуту, в пик до 250 тысяч). Это означало, что в период Черной пятницы и прочих распродаж из-за дополнительного роста нагрузки возникал риск, что CDP-сервис окажется недоступен и не получит данные интернет-магазина, который с ним интегрирован.

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

Методы интеграции с Mindbox


Как отмечено выше, Mindbox предлагает использовать для подключения API или JavaScript SDK. Далее рассмотрим их особенности.

  • JavaScript SDK


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

Ограничения: возможна потеря данных, если Mindbox недоступен в момент отправки. Скрипты платформы не подгрузятся, если на стороне интернет-магазина есть js-ошибки.

  • Интеграция по API


Интеграцию магазина с Mindbox можно провести через API. Этот способ снижает зависимость от JavaScript и также подходит для настройки асинхронной отправки данных.

Ограничения: мы столкнулись с тем, что не получали некоторые данные cookie, а именно уникальный идентификатор пользователя на устройстве (mindboxDeviceUUID). Его необходимо передавать в большинстве операций Mindbox для склеивания информации по пользователю.

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

Комбинированный метод


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

Поэтому мы обратились к третьему, комбинированному методу: работаем и с API, и с JavaScript SDK, используя наш модуль очередей.

С помощью Javascript SDK мы идентифицируем пользователя на сайте (mindboxDeviceUUID). Затем на стороне сервера формируем запрос со всеми необходимыми данными и помещаем его в очередь. Запросы из очереди через API отправляются сервису Mindbox. В случае отрицательного ответа запрос повторно помещается в очередь. Таким образом, при отправке данных Mindbox получает полный комплект необходимой информации.

В приведенном далее примере класс Sender позволяет собрать и отправить запрос, выполнив первичную обработку ответа. Класс использует данные из самой команды (тип запроса/ответа, deviceUUID и др.) и из настроек модуля (параметры работы с API, токены и т.п.).

<?phpdeclare(strict_types=1);namespace Simbirsoft\MindBox;use Bitrix\Main\Web\Uri;use Bitrix\Main\Web\HttpClient;use Simbirsoft\Base\Converters\ConverterFactory;use Simbirsoft\MindBox\Contracts\SendableCommand;class Sender{    /** @var Response Тело ответа */    protected $response;    /** @var SendableCommand Команда */    protected $command;    /**     * Sender constructor.     *     * @param SendableCommand $command     */    public function __construct(SendableCommand $command)    {        $this->command = $command;    }    /**     * Сформировать массив заголовков запроса.     *     * @return array     */    protected function getHeaders(): array    {        return [            'Accept'        => Type\ContentType::REQUEST[$this->command->getRequestType()],            'Content-Type'  => Type\ContentType::RESPONSE[$this->command->getResponseType()],            'Authorization' => 'Mindbox secretKey="'. Options::get('secretKey') .'"',            'User-Agent'    => $this->command->getHttpInfo('HTTP_USER_AGENT'),            'X-Customer-IP' => $this->command->getHttpInfo('REMOTE_ADDR'),        ];    }    /**     * Сформировать адрес запроса.     *     * @return string     */    protected function getUrl(): string    {        $uriParts = [            Options::get('apiUrl'),            $this->command->getOperationType(),        ];        $uriParams = [            'operation'  => $this->command->getOperation(),            'endpointId' => Options::get('endpointId'),        ];        $deviceUUID = $this->command->getHttpInfo('deviceUUID');        if (!empty($deviceUUID)) {            $uriParams['deviceUUID'] = $deviceUUID;        }        return (new Uri(implode('/', $uriParts)))            ->addParams($uriParams)            ->getUri();    }    /**     * Отправить запрос.     *     * @return bool     */    public function send(): bool    {        $httpClient = new HttpClient();        $headers = $this->getHeaders();        foreach ($headers as $name => $value) {            $httpClient->setHeader($name, $value, false);        }        $encodedData = null;        $request = $this->command->getRequestData();        if (!empty($request)) {            $converter = ConverterFactory::factory($this->command->getRequestType());            $encodedData = $converter->encode($request);        }        $url = $this->getUrl();        if ($httpClient->query($this->command->getMethod(), $url, $encodedData)) {            $converter = ConverterFactory::factory($this->command->getResponseType());            $response = $converter->decode($httpClient->getResult());            $this->response = new Response($response);            return true;        }        return false;    }    /**     * @return Response     */    public function getResponse(): Response    {        return $this->response;    }}


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

<?phpdeclare(strict_types=1);namespace Simbirsoft\MindBox\Traits;use RuntimeException;use Bitrix\Main\Context;use Simbirsoft\MindBox\Type;use Simbirsoft\MindBox\Sender;use Simbirsoft\MindBox\Response;use Bitrix\Main\Localization\Loc;use Simbirsoft\MindBox\Contracts\SendableCommand;Loc::loadMessages($_SERVER['DOCUMENT_ROOT'] .'/local/modules/simbirsoft.base/lib/Contracts/Command.php');trait Sendable{    /** @var string Метод отправки (GET/POST) */    protected $method = Type\OperationMethod::POST;    /** @var string Тип операции (sync/async) */    protected $operationType = Type\OperationType::ASYNC;    /** @var string Тип запроса (json/xml) */    protected $requestType = Type\ContentType::JSON;    /** @var string Тип ответа (json/xml) */    protected $responseType = Type\ContentType::JSON;    /** @var array Вспомогательные данные */    protected $data = [];    /**     * Название операции.     * @return string     */    abstract public function getOperation(): string;    /**     * Формируем данные.     *     * @return array     */    abstract public function getRequestData(): array;    /**     * HTTP метод запроса     *     * @return string     */    public function getMethod(): string    {        return $this->method;    }    /**     * Тип операции     *     * @return string     *     * @noinspection PhpUnused     */    public function getOperationType(): string    {        return $this->operationType;    }    /**     * Тип запроса.     *     * @return string     *     * @noinspection PhpUnused     */    public function getRequestType(): string    {        return $this->requestType;    }    /**     * Тип ответа.     *     * @return string     *     * @noinspection PhpUnused     */    public function getResponseType(): string    {        return $this->responseType;    }    /**     * Вспомогательные данные запроса     *     * @return void     */    public function initHttpInfo(): void    {        $server = Context::getCurrent()->getServer();        $request = Context::getCurrent()->getRequest();        $this->data = [            'X-Customer-IP' => $server->get('REMOTE_ADDR'),            'User-Agent'    => $server->get('HTTP_USER_AGENT'),            'deviceUUID'    => $request->getCookieRaw('mindboxDeviceUUID'),        ];    }    /**     * Получить вспомогательные данные запроса     *     * @param string $key     * @param string $default     *     * @return string     *     * @noinspection PhpUnused     */    public function getHttpInfo(string $key, string $default = ''): string    {        return $this->data[$key] ?? $default;    }    /**     * Выполняем команду.     *     * @return void     *     * @throws RuntimeException     */    public function execute(): void    {        /** @var SendableCommand $thisCommand */        $thisCommand = $this;        $sender = new Sender($thisCommand);        if ($sender->send()) {            throw new RuntimeException(Loc::getMessage('BASE_COMMAND_NOT_EXECUTED'));        }        $response = $sender->getResponse();        if (!$response->isSuccess()) {            throw new RuntimeException(Loc::getMessage('BASE_COMMAND_NOT_EXECUTED'));        }        if (!$this->prepareResponse($response)) {            throw new RuntimeException(Loc::getMessage('BASE_COMMAND_NOT_EXECUTED'));        }    }    /**     * Обработка ответа запроса.     *     * @param Response $response     *     * @return bool     */    public function prepareResponse(Response $response): bool    {        // $body   = $response->getBody();        // $status = $body['customer']['processingStatus'];        /**         * Возможные статусы:         * AuthenticationSucceeded - Если пароль верен         * AuthenticationFailed         - Если пароль не верен         * NotFound                          - Если потребитель не найден         */        return true;    }}


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

<?phpdeclare(strict_types=1);namespace Simbirsoft\MindBox\Commands;use Simbirsoft\Queue\Traits\Queueable;use Simbirsoft\MindBox\Traits\Sendable;use Simbirsoft\Queue\Contracts\QueueableCommand;use Simbirsoft\MindBox\Contracts\SendableCommand;final class AuthorizationCommand implements QueueableCommand, SendableCommand{    use Queueable, Sendable;    /** @var array Данные пользователя */    protected $user;    /**     * AuthorizationCommand constructor.     *     * @param array $user     */    public function __construct(array $user)    {        $keys = ['ID', 'EMAIL', 'PERSONAL_MOBILE'];        $this->user = array_intersect_key($user, array_flip($keys));        $this->initHttpInfo();    }    /**     * Название операции.     *     * @return string     */    public function getOperation(): string    {        return 'AuthorizationOnWebsite';    }    /**     * Формируем данные.     *     * @return array     */    public function getRequestData(): array    {        return [            'customer' => [                'email' => $this->user['EMAIL'],            ],        ];    }}


Схема взаимодействия модулей


В нашем проекте мы выделили три модуля:

  • Базовый


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

  • Модуль очередей


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

  • Модуль интеграции с Mindbox


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



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

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

Подводя итоги


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

В нашем примере в документации Mindbox были описаны два основных способа подключения: через Javascript SDK и через API. Для повышения надежности передачи данных, даже в случае временной недоступности CDP-сервиса, мы выбрали и реализовали третий, комбинированный способ: с помощью API и Javascript SDK, с асинхронной отправкой данных.

Спасибо за внимание! Надеемся, эта статья была для вас полезна.
Подробнее..

Зачем нам вулканец на борту обзор Spock Framework

28.08.2020 16:14:16 | Автор: admin
Автоматизация тестирования помогает постоянно контролировать качество IT-продукта, а также снижать затраты в долгосрочной перспективе. В автоматизации существуют различные подходы, например, Behavior Driven Development (BDD), разработка через поведение.

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

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



Spock framework


Spock фреймворк для тестирования и спецификации приложений на языках Java и Groovy. Благодаря использованию в качестве основы платформы JUnit этот фреймворк совместим со всеми популярными IDE (в частности, IntelliJ IDEA), различными инструментами сборки (Ant, Gradle, Maven) и continuous integration (CI) серверами.

Как пишут разработчики фреймворка, Spock вдохновлен JUnit, RSpec, jMock, Mockito, Groovy, Scala, вулканцами и другими увлекательными формами жизни.

В этой статье мы рассмотрим последнюю доступную версию, Spock Framework 2.0. Ее особенности: возможность использования JUnit5, Java 8+, groovy 2.5 (также существует сборка с версией 3.0). Spock распространяется по лицензии Apache 2.0 и имеет отзывчивое сообщество пользователей. Разработчики фреймворка продолжают дорабатывать и развивать Spock, который уже включает в себя множество расширений, позволяющих тщательно настроить запуск тестов. Например, одно из наиболее интересных анонсированных направлений доработки это добавление параллельного исполнения тестов.

Groovy


Groovy является объектно-ориентированным языком программирования, разработанным для платформы Java как дополнение с возможностями Python, Ruby и Smalltalk. Groovy использует Java-подобный синтаксис с динамической компиляцией в JVM байт-код и напрямую работает с другим Java-кодом и библиотеками. Язык может использоваться в любом Java-проекте или как скриптовый язык.

К особенностям groovy относятся: как статическая, так и динамическая типизация; встроенный синтаксис для списков, массивов и регулярных выражений; перегрузка операций. При этом замыкания в Groovy появились задолго до Java.

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

Особенности Spock Framework


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

Спецификация представляет собой класс groovy, расширяющий spock.lang.Specification

class MyFirstSpecification extends Specification {  // fields  // fixture methods  // feature methods  // helper methods}

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

С помощью аннотации @Shared можно дать доступ к полю классам-наследникам спецификации.

abstract class PagesBaseSpec extends Specification {    @Shared    protected WebDriver driver    def setup() {        this.driver = DriverFactory.createDriver()        driver.get("www.anywebservice.ru")    }    void cleanup() {        driver.quit()    }}


Методы настройки класса спецификации:

def setupSpec() {} // запускается при работе первого feature метода из спецификации def setup() {}     // запускается перед каждым feature методомdef cleanup() {}   // запускается после каждого feature методаdef cleanupSpec() {} // запускается после работы последнего feature метода из спецификации

В следующей таблице рассмотрим, у каких ключевых слов и методов Spock framework есть аналоги в JUnit.



Блоки теста


В Spock Framework каждая фаза теста выделена в отдельный блок кода (см. пример в документации).



Блок кода начинается с лейбла и завершается началом следующего блока кода или окончанием теста.

Блок given отвечает за настройку начальных условий теста.

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

В тех случаях, когда есть возможность сократить конструкцию when-then до одного выражения, можно использовать один блок expect. Далее будут использованы примеры из официальной документации Spock framework:

when:def x = Math.max(1, 2) then:x == 2

или одно выражение

expect:Math.max(1, 2) == 2

Блок cleanup применяют для освобождения ресурсов перед следующей итерацией теста.

given:def file = new File("/some/path")file.createNewFile() // ... cleanup:file.delete()

Блок where применяют для передачи данных для тестирования (Data Driven Testing).

def "computing the maximum of two numbers"() {  expect:  Math.max(a, b) == c   where:  a << [5, 3]  b << [1, 9]  c << [5, 9]}

Виды передачи входных данных будут рассмотрены далее.

Пример реализации теста на Spock Framework


Далее рассмотрим подходы к реализации тестирования веб-страницы авторизации пользователя в системе с использованием selenium.

import helpers.DriverFactoryimport org.openqa.selenium.WebDriverimport spock.lang.Sharedimport spock.lang.Specificationabstract class PagesBaseSpec extends Specification {    @Shared    protected WebDriver driver        def setup() {        this.driver = DriverFactory.createDriver()        driver.get("www.anywebservice.ru")    }    void cleanup() {        driver.quit()    }}

Здесь мы видим базовый класс спецификации страницы. В начале класса мы видим импорт необходимых классов. Далее представлена аннотация shared, позволяющая классам-наследникам получить доступ к веб-драйверу. В блоке setup() мы видим код инициализации веб-драйвера и открытия веб-страницы. В блоке cleanup() код завершения работы веб-драйвера.

Далее перейдем к обзору спецификации страницы авторизации пользователя.

import pages.LoginPageimport spock.lang.Issueclass LoginPageTest extends PagesBaseSpec {    @Issue("QAA-1")    def "QAA-1: Authorization with correct login and password"() {        given: "Login page"        def loginPage = new LoginPage(driver)        and: "Correct login and password"        def adminLogin = "adminLogin"        def adminPassword = "adminPassword"        when: "Log in with correct login and password"        loginPage.login(adminLogin, adminPassword)        then: "Authorized and moved to main page"        driver.currentUrl == "www.anywebservice.ru/main"    }}

Спецификация страницы авторизации наследуется от базовой спецификации страниц. Аннотация Issue задает идентификатор теста во внешней системе трекинга (например, Jira). В следующей строке мы видим название теста, которое по соглашению задается строковыми литералами, что позволяет использовать любые символы в названии теста (в том числе и русскоязычные). В блоке given происходит инициализация page object класса страницы авторизации, а также получение корректных логина и пароля для авторизации в системе. В блоке when выполняется действие по авторизации. В блоке then проверка ожидаемого действия, а именно успешная авторизация и переадресация на главную страницу системы.

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

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

Data Driven Testing в Spock Framework


Data Driven Testing = table-driven testing = parameterized testing

Для тестирования сценария с несколькими параметрами можно использовать различные варианты их передачи.

Таблицы данных (Data Tables)


Рассмотрим несколько примеров из официальной документации фреймворка.

class MathSpec extends Specification {  def "maximum of two numbers"() {    expect:    Math.max(a, b) == c     where:    a | b | c    1 | 3 | 3    7 | 4 | 7    0 | 0 | 0  }}

Каждая строка в таблице отдельная итерация теста. Также таблица может быть представлена и одним столбцом.

where:a | _1 | _7 | _0 | _

_ объект-заглушка класса спецификации.

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

def "maximum of two numbers"() {    expect:    Math.max(a, b) == c     where:    a | b || c    1 | 3 || 3    7 | 4 || 7    0 | 0 || 0}
Теперь мы видим, что a, b входные параметры, а c ожидаемое значение.

Потоки данных (Data pipes)


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

...where:a << [1, 7, 0]b << [3, 4, 0]c << [3, 7, 0]

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

Для каждой итерации теста будут запрашиваться следующие данные из списка для каждой переменной:

1 итерация: a=1, b=3, c=3;
2 итерация: a=7, b=4, c=7;
3 итерация: a=0, b=0, c=0.

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

@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver") def "maximum of two numbers"() {  expect:  Math.max(a, b) == c   where:  [a, b, c] << sql.rows("select a, b, c from maxdata")}


Переменная как данные (Data Variable Assignment)


...where:a = 3b = Math.random() * 100c = a > b ? a : b

Здесь мы видим динамически вычисляемую переменную c в тестовых данных.

Комбинация различных видов передачи параметров


...where:a | _3 | _7 | _0 | _ b << [5, 0, 0] c = a > b ? a : b

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

Пример реализации параметризованного теста на Spock Framework


@Issue("QAA-1-parametrized")def "QAA-1-parametrized: Authorization with correct login and password"() {   given: "Login page"   def loginPage = new LoginPage(driver)   when: "Log in with correct login and password"   loginPage.login(login, password)   then: "Authorized and moved to main page"   driver.currentUrl =="www.anywebservice.ru/main"   where: "Check for different logins and passwords"   login            | password   "adminLogin"     | "adminPassword"   "moderatorLogin" | "moderatorPassword"   "userLogin"      | "userPassword"}

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

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

Пример реализации параметризованного теста с доработанной спецификацией


До доработки


abstract class PagesBaseSpec extends Specification {    @Shared    protected WebDriver driver    def setup() {        this.driver = DriverFactory.createDriver()        driver.get("www.anywebservice.ru")    }    void cleanup() {        driver.quit()    }}


После доработки


import helpers.DriverFactoryimport org.openqa.selenium.WebDriverimport spock.lang.Sharedimport spock.lang.Specificationabstract class PagesNoRestartBaseSpec extends Specification {    @Shared    protected WebDriver driver    def setupSpec() {        this.driver = DriverFactory.createDriver()    }    def setup() {        this.driver.get("www.anywebservice.ru")    }    def cleanup() {        this.driver.get("www.anywebservice.ru/logout")        this.driver.manage().deleteAllCookies();    }    void cleanupSpec() {        this.driver.quit()    }}

В обновленной спецификации мы видим, что процедура создания веб-драйвера будет выполняться только при настройке класса спецификации, а закрытие браузера только после завершения работы тестов из спецификации. В методе setup() мы видим тот же код получения веб-адреса сервиса и его открытие в браузере, а в методе cleanup() переход по адресу www.anywebservice.ru/logout для завершения работы с сервисом у текущего пользователя и удаления файлов куки (для тестирования текущего веб-сервиса данной процедуры достаточно, чтобы имитировать уникальный запуск). Код самого теста не изменился.

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

Сравнение тестов на testNG, pytest, pytest-bdd


Для начала мы рассмотрим реализацию теста на тестовом фреймворке testNG на языке программирования Java, который также, как и Spock Framework, вдохновлен фреймворком jUnit и поддерживает data-driven testing.

package javaTests;import org.testng.Assert;import org.testng.annotations.*;import pages.LoginPage;public class LoginPageTest extends BaseTest {    @BeforeClass    public final void setup() {        createDriver();        driver.get("www.anywebservice.ru");    }    @DataProvider(name = "userParameters")    public final Object[][] getUserData(){        return new Object[][] {                {"adminLogin", "adminPassword"},                {"moderatorLogin", "moderatorPassword"},                {"userLogin", "userPassword"}        };    }    @Test(description = "QAA-1-1: Authorization with correct login and password",            dataProvider = "userParameters")    public final void authorizationWithCorrectLoginAndPassword(String login, String password){        //Login page        LoginPage loginPage = new LoginPage(driver);        //Log in with correct login and password        loginPage.login(login, password);        //Authorized and moved to main page        Assert.assertEquals("www.anywebservice.ru/main", driver.getCurrentUrl());    }    @AfterMethod    public final void cleanup() {        driver.get("www.anywebservice.ru/logout");        driver.manage().deleteAllCookies();    }    @AfterClass    public final void tearDown() {        driver.quit();    }}

Здесь мы можем видеть тестовый класс со всеми необходимыми setup(), cleanup() методами, а также параметризацию теста в виде дополнительного метода getUserData() с аннотацией @DataProvider, что выглядит несколько громоздко, после того, что мы рассмотрели в тесте с использованием Spock Framework. Также для понимания того, что происходит в тесте, были оставлены комментарии, аналогичные описанию шагов.

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



Далее перейдем к тесту с использованием тестового фреймворка pytest на языке программирования Python.

import pytestfrom selenium.webdriver.support import expected_conditionsfrom selenium.webdriver.support.wait import WebDriverWaitfrom PageObjects.LoginPage import LoginPageclass TestLogin(object):    @pytest.mark.parametrize("login,password", [        pytest.param(("adminLogin", "adminPassword"), id='admin'),        pytest.param(("moderatorLogin", "moderatorPassword"), id='moderator'),        pytest.param(("userLogin", "userPassword"), id='user')    ])    def test_authorization_with_correct_login_and_password(self, login, password, driver, test_cleanup):        # Login page        login_page = LoginPage(driver)        # Log in with correct login and password        login_page.login(login, password)        # Authorized and moved to main page        assert expected_conditions.url_to_be("www.anywebservice.ru/main")     @pytest.fixture()    def test_cleanup(self, driver):        yield "test"        driver.get("www.anywebservice.ru/logout")        driver.delete_all_cookies()

Здесь мы также видим поддержку data-driven testing в виде отдельной конструкции, схожей с @DataProvider в testNG. Метод настройки веб-драйвера спрятан в фикстуре driver. Благодаря динамической типизации и фикстурам pytest, код этого теста выглядит чище, чем на Java.



Далее перейдем к обзору кода теста с использованием плагина pytest-bdd, который позволяет писать тесты в виде feature файлов Gherkin (чистый BDD-подход).

login.feature

Feature: Login page  A authorization  Scenario: Authorizations with different users    Given Login page    When Log in with correct login and password    Then Authorized and moved to main page


test_login.py

import pytestfrom pytest_bdd import scenario, given, when, thenfrom selenium.webdriver.support import expected_conditionsfrom selenium.webdriver.support.wait import WebDriverWaitfrom PageObjects.LoginPage import LoginPage@pytest.mark.parametrize("login,password", [    pytest.param(("adminLogin", "adminPassword"), id='admin'),    pytest.param(("moderatorLogin", "moderatorPassword"), id='moderator'),    pytest.param(("userLogin", "userPassword"), id='user')])@scenario('login.feature', 'Authorizations with different users')def test_login(login, password):    pass@given('Login page')def login_page(driver):    return LoginPage(driver)@when('Log in with correct login and password')def login_with_correct_login_and_password(login_page, login, password):    login_page_object = login_page    login_page_object.login(login, password)@then('Authorized and moved to main page')def authorized_and_moved_to_main_page(driver, login):    assert expected_conditions.url_to_be("www.anywebservice.ru/main")

Из плюсов можно выделить то, что это все еще фреймворк pytest, который имеет множество плагинов для различных ситуаций, в том числе и для параллельного запуска тестов. Из минусов сам чистый BDD-подход, который будет постоянно ограничивать разработчика своими особенностями. Spock Framework дает возможность писать более лаконичный и простой в оформлении код, по сравнению со связкой PyTest + pytest-bdd.



Заключение


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

Плюсы:

  • Использование принципов BDD вместо чистого BDD-подхода дает большую гибкость при написании тестов.
  • Написанная тестовая спецификация является также и документацией системы.
  • Наличие различных расширений для настройки тестов.
  • Язык groovy (динамическая типизация, синтаксический сахар, closures или замыкания).


Минусы:

  • Динамическая типизация языка groovy. Поскольку применяется динамическая типизация, то механизмы предугадывания, используемые в IDE для анализа содержимого переменной, при долгой работе могут начать сбоить. Если рассматривать Intellij IDEA, то постоянно ведутся доработки в этом направлении, что, несомненно, радует.
  • Динамическая компиляция groovy кода в JVM байт-код. Если кратко, то не стоит писать все подряд на groovy, поскольку вы можете существенно проиграть во времени компиляции данного кода, особенно если он занимает много строк кода и часто используется. Важные части своего тестового фреймворка все же стоит писать на java, а groovy оставить для тестов.
  • Набор расширений не такой обширный, как у testNG, к примеру. Как следствие отсутствие параллельного запуска тестов. Есть планы добавить эту функциональность, но сроки их реализации неизвестны.

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

Что еще можно почитать:
Подробнее..

Оценка трудозатрат в веб- и мобильных проектах

12.02.2021 10:14:08 | Автор: admin

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

Как происходит оценка проекта

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

  • созваниваться с клиентом для уточнения требований, согласования вариантов решения и стека технологий;

  • курировать процесс оценки;

  • предлагать варианты реализации;

  • оценивать и закладывать риски;

  • определять состав команды разработки;

  • составлять предварительный роадмап проекта.

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

  • требования клиента изменились, например, он подготовил более детальное ТЗ и макеты;

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

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

Риски и трудозатраты на управление

При оценке нужно учитывать трудозатраты на управление проектом (Project Management или PM), в том числе:

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

  • проведение митингов;

  • коммуникации с заказчиком продукта и устранение препятствий;

  • ретроспективы с командой;

  • демо с заказчиком;

  • работа с метриками и оценка рисков.

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

  • команда часто перерабатывает, причем больше, чем на 10-15%;

  • ТЗ постоянно меняется, а тестовая документация при этом не обновляется;

  • фактическое время выполнения задачи сильно превышает планируемое.

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

Оценка трудозатрат

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

  1. Приемка приложения

  2. Аналитика и дизайн

  3. Непосредственная разработка и интеграция с другими IT-системами

  4. Тестирование и обеспечение качества (QA)

Мы поставили перед собой задачу выяснить, как в среднем распределяются трудозатраты по фазам разработки. Для этого мы проанализировали выборку веб- и мобильных MVP-проектов, разработанных в 2020 году. Трудозатраты на их реализацию составили от 2000 до 4500 часов.

Веб-приложение

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

  • Аналитика 7%

  • Backend 30%

  • Frontend 24,6%

  • Тестирование 15%

  • Управление 22%

Как показывает практика, разработка занимает около 50-60% от общих сроков реализации веб-приложения.

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

Мобильная разработка

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

В веб-проектах трудозатраты распределились так:

  • Аналитика 3,8%

  • Backend 18,9%

  • iOS 16,1%

  • Android 25,8%

  • Тестирование 25,1%

  • Управление 10,3%

По нашим оценкам, Backend- и мобильная разработка в среднем составили около 60% трудозатрат.

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

Особенности расчетов

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

Пример оценки в EstimateПример оценки в Estimate

Заключение

В этой статье мы рассчитали особенности распределения трудозатрат в разных видах продуктов. В наших веб-проектах разработка составила в среднем 50% от общих трудозатрат, а в мобильных проектах около 60%.

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

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

Подробнее..

5 мифов о тимлидах. Как стать тимлидом и избежать ошибок

19.11.2020 16:18:00 | Автор: admin
Привет, Хабр! Один из вечных споров в IT о том, как развиваться разработчику: прокачивать хардскиллы или навыки управленца? Если и вы задаете себе этот вопрос, давайте вспомним 5 известных мифов о работе тимлида и конечно, сравним их с реальностью.

Меня зовут Александр, я возглавляю TeamLead-направление в SimbirSoft. Приведу примеры, какие у тимлидов задачи и что можно сделать, чтобы наладить обмен опытом как между собой, так и со всеми, кто готов учиться.



image alt
Я уже более года веду наш внутренний проект по обучению Академию тимлидов. Наши менторы помогают Middle-разработчикам, которые настроены на получение управленческих навыков. За год обучение прошли 80 специалистов, и половина из них уже работают на проектах.

Что показывает практика:


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

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



Миф 1. Тимлид самый крутой технарь в команде


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

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

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

Пример: бывает, что в проектной команде задействованы разработчики разных направлений, с разным опытом Backend, Frontend и Mobile, QA-специалисты, SDET и DevOps. Даже если вы считаете, что разработчикам нужно стремиться к самоорганизации, обычно это недостижимый идеал. В жизни команде зачастую нужен классический лидер.

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

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



Миф 2. Тимлид не развивается как технарь и выгорает


Соотношение технических задач к управленческим может быть разным например, 70/30, 80/20 или даже 50/50.

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

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

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



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

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


Миф 3. Тимлид должен кодить сам и лучше всех


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

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

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



Миф 4. Тимлид в одиночку отвечает за весь проект


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



Рассмотрим, какие задачи решают тимлиды в распределенных командах, на примере одного из наших проектов доработки крупной банковской IT-системы.

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

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

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

  • провели 5 тимбилдингов распределенных команд и обеспечили благоприятный климат в команде;
  • команда увеличила скорость выполнения задач на 10% за счет налаженных коммуникаций;
  • сократили процесс настройки окружения с 3 дней до 4 часов.

На этом примере мы убедились, как значительно работа тимлида может повлиять на процессы в команде. За несколько лет сотрудничества наша команда на проекте расширилась до более чем 60 специалистов: backend-, frontend- и мобильных разработчиков, аналитиков, QA.

Миф 5. Разработчики переходят в тимлиды, чтобы повысить свою зарплату


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

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



Академия тимлидов: наш опыт


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

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

Например, у нас есть Академия тимлидов внутренний проект для передачи опыта. С 2019 года мы обучаем тех разработчиков, которые интересуются управлением командами. За год обучение прошли 80 специалистов, половина из них более 40 человек уже погрузились в новую роль и подключились к проектным командам.

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

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

Что такое Академия тимлидов

Наш практикум это 21 онлайн-консультация (более 50 часов) и 5 месяцев погружения в профессию. Онлайн-консультации проходят один раз в неделю.

26 ноября 2020 года ближайший практикум, а также открытый вебинар (регистрация здесь).

Для кого:


  • Для тех, кто хочет стать тимлидом.
  • Для тех, кто уже руководит командами разработчиков.
  • Для разработчиков Middle/Senior, которые хотят прокачать в себе управленческие навыки.

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

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

Распознаем номера автомобилей. Разработка multihead-модели в Catalyst

11.06.2021 08:06:47 | Автор: admin

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

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

Сделать модель для распознавания можно с помощью разных подходов, например, путем поиска и определения отдельных символов, или в виде задачи image-to-text. Мы рассмотрим модель с несколькими выходами (multihead-модель). В качестве датасета возьмём датасет с российскими номерами от проекта Nomeroff Net. Примеры изображений из датасета представлены на рис. 1.

Рис. 1. Примеры изображений из датасета

Общий подход к решению задачи

Необходимо разработать модель, которая на входе будет принимать изображение ГРЗ, а на выходе отдавать строку распознанных символов. Модель будет состоять из экстрактора фичей и нескольких классификационных голов. В датасете представлены ГРЗ из 8 и 9 символов, поэтому голов будет девять. Каждая голова будет предсказывать один символ из алфавита 1234567890ABEKMHOPCTYX, плюс специальный символ - (дефис) для обозначения отсутствия девятого символа в восьмизначных ГРЗ. Архитектура схематично представлена на рис. 2.

Рис. 2. Архитектура модели

В качестве loss-функции возьмём стандартную кросс-энтропию. Будем применять её к каждой голове в отдельности, а затем просуммируем полученные значения для получения общего лосса модели. Оптимизатор Adam. Используем также OneCycleLRWithWarmup как планировщик leraning rate. Размер батча 128. Длительность обучения установим в 10 эпох.

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

Кодирование

Далее рассмотрим основные моменты кода. Класс датасета (листинг 1) в общем обычный для CV-задач на Pytorch. Обратить внимание стоит лишь на то, как мы возвращаем список кодов символов в качестве таргета. В параметре label_encoder передаётся служебный класс, который умеет преобразовывать символы алфавита в их коды и обратно.

class NpOcrDataset(Dataset):   def __init__(self, data_path, transform, label_encoder):       super().__init__()       self.data_path = data_path       self.image_fnames = glob.glob(os.path.join(data_path, "img", "*.png"))       self.transform = transform       self.label_encoder = label_encoder    def __len__(self):       return len(self.image_fnames)    def __getitem__(self, idx):       img_fname = self.image_fnames[idx]       img = cv2.imread(img_fname)       if self.transform:           transformed = self.transform(image=img)           img = transformed["image"]       img = img.transpose(2, 0, 1)             label_fname = os.path.join(self.data_path, "ann",                                  os.path.basename(img_fname).replace(".png", ".json"))       with open(label_fname, "rt") as label_file:           label_struct = json.load(label_file)           label = label_struct["description"]       label = self.label_encoder.encode(label)        return img, [c for c in label]

Листинг 1. Класс датасета

В классе модели (листинг 2) мы используем библиотеку PyTorch Image Models для создания экстрактора фичей. Каждую из классификационных голов модели мы добавляем в ModuleList, чтобы их параметры были доступны оптимизатору. Логиты с выхода каждой из голов возвращаются списком.

class MultiheadClassifier(nn.Module):   def __init__(self, backbone_name, backbone_pretrained, input_size, num_heads, num_classes):       super().__init__()        self.backbone = timm.create_model(backbone_name, backbone_pretrained, num_classes=0)       backbone_out_features_num = self.backbone(torch.randn(1, 3, input_size[1], input_size[0])).size(1)        self.heads = nn.ModuleList([           nn.Linear(backbone_out_features_num, num_classes) for _ in range(num_heads)       ])     def forward(self, x):       features = self.backbone(x)       logits = [head(features) for head in self.heads]       return logits

Листинг 2. Класс модели

Центральным звеном, связывающим все компоненты и обеспечивающим обучение модели, является Runner. Он представляет абстракцию над циклом обучения-валидации модели и отдельными его компонентами. В случае обучения multihead-модели нас будет интересовать реализация метода handle_batch и набор колбэков.

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

class MultiheadClassificationRunner(dl.Runner):   def __init__(self, num_heads, *args, **kwargs):       super().__init__(*args, **kwargs)       self.num_heads = num_heads    def handle_batch(self, batch):       x, targets = batch       logits = self.model(x)             batch_dict = { "features": x }       for i in range(self.num_heads):           batch_dict[f"targets{i}"] = targets[i]       for i in range(self.num_heads):           batch_dict[f"logits{i}"] = logits[i]             self.batch = batch_dict

Листинг 3. Реализация runnerа

Колбэки мы будем использовать следующие:

  • CriterionCallback для расчёта лосса. Нам потребуется по отдельному экземпляру для каждой из голов модели.

  • MetricAggregationCallback для агрегации лоссов отдельных голов в единый лосс модели.

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

  • SchedulerCallback для запуска LR Schedulerа.

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

  • CheckpointCallback чтобы сохранять лучшие веса модели.

Код, формирующий список колбэков, представлен в листинге 4.

def get_runner_callbacks(num_heads, num_classes_per_head, class_names, logdir):   cbs = [       *[           dl.CriterionCallback(               metric_key=f"loss{i}",               input_key=f"logits{i}",               target_key=f"targets{i}"           )           for i in range(num_heads)       ],       dl.MetricAggregationCallback(           metric_key="loss",           metrics=[f"loss{i}" for i in range(num_heads)],           mode="mean"       ),       dl.OptimizerCallback(metric_key="loss"),       dl.SchedulerCallback(),       *[           dl.AccuracyCallback(               input_key=f"logits{i}",               target_key=f"targets{i}",               num_classes=num_classes_per_head,               suffix=f"{i}"           )           for i in range(num_heads)       ],       dl.CheckpointCallback(           logdir=os.path.join(logdir, "checkpoints"),           loader_key="valid",           metric_key="loss",           minimize=True,           save_n_best=1       )   ]     return cbs

Листинг 4. Код получения колбэков

Остальные части кода являются тривиальными для Pytorch и Catalyst, поэтому мы не станем приводить их здесь. Полный код к статье доступен на GitHub.

Результаты эксперимента

Рис. 3. График лосс-функции модели в процессе обучения. Оранжевая линия train loss, синяя valid loss

В списке ниже перечислены некоторые ошибки, которые модель допустила на тест-сете:

  • Incorrect prediction: T970XT23- instead of T970XO123

  • Incorrect prediction: X399KT161 instead of X359KT163

  • Incorrect prediction: E166EP133 instead of E166EP123

  • Incorrect prediction: X225YY96- instead of X222BY96-

  • Incorrect prediction: X125KX11- instead of X125KX14-

  • Incorrect prediction: X365PC17- instead of X365PC178

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

Заключение

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

Спасибо за внимание! Надеемся, что наш опыт был вам полезен.

Больше наших статей по машинному обучению и обработке изображений:

Подробнее..

От монолита к микросервисам ускорили банковские релизы в 15 раз

24.07.2020 12:09:40 | Автор: admin
Бывает, что компания использует устаревшую монолитную IT-систему, с которой сложно быстро выпускать обновления и решать свои бизнес-задачи. Как правило, рано или поздно владелец продукта начинает проектировать новое, более гибкое архитектурное решение.

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



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

Проблемы коробочного монолита


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

Именно такую коробочную систему дистанционного банковского обслуживания (ДБО) использовал один из наших клиентов. Онлайн-банк представлял собой монолитное приложение с достаточно небольшим набором функций.

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

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

  1. Вендор вносил желаемые изменения неприемлемо долго, а переписать мобильный интерфейс в конкретной коробке и вовсе было практически невозможно.
  2. Из-за сбоев в шине данных или коробке пользователи зачастую не могли войти в онлайн-банк.
  3. Работать с приложением пользователи могли только в том случае, если у них была установлена самая свежая версия.
  4. Для распределения нагрузки инстансы монолита были развернуты на нескольких серверах, каждый из которых обслуживал свою группу абонентов. При падении одного из серверов пропадал доступ у всех абонентов соответствующей группы.
  5. Вся экспертиза хранилась у производителя коробочного решения, а не в банке.
  6. Из-за того, что банк не мог оперативно внести изменения по просьбам пользователей, а функциональность была недостаточной по сравнению с конкурентами, появился риск оттока клиентов.

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

С чего мы начинали


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

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

Архитектура

Вместе с владельцем продукта мы приняли решение использовать Spring Cloud, который предоставляет все необходимые функции для реализации микросервисной архитектуры: это и Service discovery Eureka, Api Gateway Zuul, Config server и многое другое. В качестве системы контейнеров для Docker-образов выбрали OpenShift, потому что инфраструктура банка была заточена под этот инструмент.

Также мы проанализировали, какие особенности старой коробки могут осложнять работу пользователей. Один из основных недостатков заключался в том, что система синхронно работала через шину данных, и каждое действие пользователей вызывало обращение к шине. Из-за больших нагрузок часто происходил отказ шины, при этом переставало работать все приложение. Кроме того, как и во многих старых банковских продуктах, накопилось легаси наследство в виде старого и тяжеловесного CORE АБС, переписывать которое было бы сложно и дорого.

Мы предложили ряд улучшений:

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

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

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

  • Асинхронность

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

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

  • Кэширование

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

Что изменилось в системе


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





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



Давайте рассмотрим пример получения данных по счетам пользователя в новой архитектуре. Запрос пользователя из мобильного приложения попадает через балансировщик на Gateway, который понимает, на какой из сервисов направить запрос. Далее попадаем на API сервис счетов. Сервис сперва проверяет, есть ли актуальные данные пользователя в Cache. При успешном исходе возвращает данные, в ином случае отправляет запрос в Middle сервис счетов.

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

Среди наиболее важных изменений можно отметить следующие:

  • Гибкость масштабирования

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

  • Ускорение релизов за счет версионирования

Ранее при обновлении мобильной версии одни пользователи уже применяли новую версию, а другие нет, но число последних было минимально. При разработке нового ДБО мы реализовали версионирование и возможность выпускать релизы без риска, что приложение перестанет работать у большой части клиентов. Сейчас при реализации отдельных функциональностей в новых версиях мы не ломаем старые версии, а значит, нет необходимости проводить регресс. Это помогло ускорить частоту релизов минимум в 15 раз теперь релизы выходят в среднем 1 раз в неделю. Команды Backend и Mobile могут одновременно и независимо работать над новыми функциональностями.

Подводя итоги


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

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

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

Спасибо за внимание!
Подробнее..

Как стать разработчиком Java и С открываем онлайн-практикум с поддержкой менторов

01.02.2021 16:08:09 | Автор: admin

Какие навыки прокачать на старте, где найти ментора, как получить первый опыт командной работы все эти вопросы знакомы разработчикам-джунам. Изучая Java или C# самостоятельно, можно запутаться в море информации и потратить больше года на первые шаги. Сократить этот путь помогают практикумы, в том числе в IT-компаниях где менторы готовы поделиться знаниями, давно накоплена база знаний и отлажены процессы разработки. Мы в SimbirSoft проводим такие практикумы несколько раз в год. Сейчас мы открыли запись на ближайший запуск 22 февраля. Рассказываем, чему научатся участники и как подать заявку.

В чём помогают практикумы

С помощью онлайн-практикумов разработчики могут комплексно решить несколько задач в обучении:

  • за 1,5-2 месяца систематизировать свои знания, занимаясь в небольших группах;

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

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

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

Как устроены наши практикумы

У нас в SimbirSoft есть разные форматы онлайн-встреч как для обучения, так и для обмена опытом:

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

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

1) ждем начинающих C#-разработчиков из Ульяновска, Самары, Саранска, Димитровграда, Казани и Краснодара;

2) начинающих Java-разработчиков из Казани и Самары.

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

- До 10 февраля принимаем тестовые задания на Весенний интенсив для Frontend-, Web- и мобильных разработчиков.

- До 11 февраля принимаем тестовые задания на онлайн-практикум по QA и тестированию в Краснодаре.

- Подписаться на новости о следующих событиях можно ВКонтакте или в Telegram.

Программа Backend-практикума

1) Трек Java

  • Spring Initializr, (Rest)Controller, Git. Как создать проект, как сделать контроллер, отдающий статику и json, как создать репозиторий и как залить в него изменения.

  • DB, Service, Repository (встроенки), Component, Configuration. Как подключить базу данных, как организовать работу с данными через сервисы и репозитории, что такое бины и компоненты, как с ними работать.

  • Security, Migrations, DB level-up. Как подключить и настроить базовую безопасность, как управлять пользователями, что такое миграции и для чего они нужны, транзакции и каскадные операции с БД.

  • Testing, Patterns, Security level-up. Как писать правильные тесты и работать с тестовыми фреймворками, какие существуют паттерны проектирования и как применять их в проектах, вопросы безопасности.

  • Spring AOP, Tips&Tricks. Что такое АОП и как этим пользоваться, работа с побочными инструментами (swagger, статические анализаторы и др.), как работать с GitHub (пулл-реквесты, projects).

2) Трек C#

  • .NET Core 3.1, Asp.Net Core, Git. Как создать проект и репозиторий, внести и залить изменения.

  • Nuget. Как подключать библиотеки к проекту и управлять зависимостями.

  • Entity Framework Core, DbUp. Как подключить базу данных, организовать работу с данными через сервисы и репозитории, как с ними работать. Что такое миграции и для чего они нужны, транзакции и каскадные операции с БД.

  • Testing, Patterns, xUnit. Как писать правильные тесты и работать с тестовыми фреймворками, какие существуют паттерны проектирования и как применять их в проектах.

  • Security, Docker. Глубже рассмотрим вопросы безопасности и автоматизации развёртывания и управления приложениями.

Владислав, куратор практикумов по Backend-направлению:

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

Алексей, разработчик C#, ментор трека C#:

В ходе нашего практикума вы сможете попробовать себя в качестве разработчика, обучиться азам профессии, а также получите опыт работы в команде, что является неотъемлемой частью IT-сферы. Полученные знания позволят оценить силу мотивации, реальные возможности и решить, нужно ли идти дальше. Менторы ответят на все ваши вопросы, помогут разобраться в трудностях и обучат собственным профессиональным приёмам.

Подключайтесь к нашему практикуму! Регистрация на TimePad до 18 февраля.

Подробнее..

Как создавать гибкие списки обзор динамического UICollectionView IGListKit

22.12.2020 12:11:10 | Автор: admin
Коллекции есть во многих мобильных приложениях например, это могут быть списки публикаций в соцсети, рецепты, формы обратной связи и многое другое. Для их создания часто используют UICollectionView. Для формирования гибкого списка нужно синхронизировать модель данных и представление, но при этом возможны различные сбои.

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



Как работать с IGListKit


Применение фреймворка IGListKit в общих чертах схоже со стандартной реализацией UICollectionView. При этом у нас есть:

  • модель данных;
  • ViewController;
  • ячейки коллекции UICollectionViewCell.

Кроме того, есть вспомогательные классы:

  • SectionController отвечает за конфигурацию ячеек в текущей секции;
  • SectionControllerModel для каждой секции своя модель данных;
  • UICollectionViewCellModel для каждой ячейки, также своя модель данных.

Рассмотрим их использование подробнее.

Создание модели данных


Для начала нам нужно создать модель, которая представляет собой класс, а не структуру. Эта особенность связана с тем, что IGListKit написан на Objective-C.

final class Company {        let id: String    let title: String    let logo: UIImage    let logoSymbol: UIImage    var isExpanded: Bool = false        init(id: String, title: String, logo: UIImage, logoSymbol: UIImage) {        self.id = id        self.title = title        self.logo = logo        self.logoSymbol = logoSymbol    }}

Теперь расширим модель протоколом ListDiffable.

extension Company: ListDiffable {    func diffIdentifier() -> NSObjectProtocol {        return id as NSObjectProtocol    }     func isEqual(toDiffableObject object: ListDiffable?) -> Bool {        guard let object = object as? Company else { return false }        return id == object.id    }}

ListDiffable позволяет однозначно идентифицировать и сравнивать объекты, чтобы безошибочно автоматически обновлять данные внутри UICollectionView.

Протокол требует реализации двух методов:

func diffIdentifier() -> NSObjectProtocol


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

func isEqual(toDiffableObject object: ListDiffable?) -> Bool


Этот метод служит для сравнения двух моделей между собой.

При работе с IGListKit принято использовать модели для создания и работы каждой из ячеек и SectionController. Эти модели создают по правилам, описанным выше. Пример можно посмотреть в репозитории.

Синхронизация ячейки с моделью данных


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

extension ExpandingCell: ListBindable {    func bindViewModel(_ viewModel: Any) {        guard let model = viewModel as? ExpandingCellModel else { return }        logoImageView.image = model.logo        titleLable.text = model.title        upDownImageView.image = model.isExpanded            ? UIImage(named: "up")            : UIImage(named: "down")    }}

Данный протокол требует реализации метода func bindViewModel(_ viewModel: Any). Этот метод обновляет данные в ячейке.

Формируем список ячеек SectionController


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

final class InfoSectionController: ListBindingSectionController<ListDiffable> {     weak var delegate: InfoSectionControllerDelegate?        override init() {        super.init()                dataSource = self    }}

Наш класс наследуется от
ListBindingSectionController<ListDiffable>

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

Также нам необходимо расширить SectionController протоколом ListBindingSectionControllerDataSource.

extension InfoSectionController: ListBindingSectionControllerDataSource {    func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable] {        guard let sectionModel = object as? InfoSectionModel else {            return []        }                var models = [ListDiffable]()                for item in sectionModel.companies {            models.append(                ExpandingCellModel(                    identifier: item.id,                    isExpanded: item.isExpanded,                    title: item.title,                    logo: item.logoSymbol                )            )                        if item.isExpanded {                models.append(                    ImageCellModel(logo: item.logo)                )            }        }                return models    }        func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell & ListBindable {         let cell = self.cell(for: viewModel, at: index)        return cell    }        func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any, at index: Int) -> CGSize {        let width = collectionContext?.containerSize.width ?? 0        var height: CGFloat        switch viewModel {        case is ExpandingCellModel:            height = 60        case is ImageCellModel:            height = 70        default:            height = 0        }                return CGSize(width: width, height: height)    }}


Для соответствия протоколу реализуем 3 метода:

func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable] 

Этот метод формирует массив моделей в порядке вывода в UICollectionView.

func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell & ListBindable 

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

func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any, at index: Int) -> CGSize 

Метод возвращает размер для каждой ячейки.

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


Подключим в имеющийся ViewController ListAdapter и модель данных, а также заполним ее. ListAdapter позволяет создавать и обновлять UICollectionView с ячейками.

class ViewController: UIViewController {     var companies: [Company]        private lazy var adapter = {        ListAdapter(updater: ListAdapterUpdater(), viewController: self)    }()        required init?(coder: NSCoder) {        self.companies = [            Company(                id: "ss",                title: "SimbirSoft",                logo: UIImage(named: "ss_text")!,                logoSymbol: UIImage(named: "ss_symbol")!            ),            Company(                id: "mobile-ss",                title: "mobile SimbirSoft",                logo: UIImage(named: "mobile_text")!,                logoSymbol: UIImage(named: "mobile_symbol")!            )        ]                super.init(coder: coder)    }        override func viewDidLoad() {        super.viewDidLoad()                configureCollectionView()    }        private func configureCollectionView() {        adapter.collectionView = collectionView        adapter.dataSource = self    }}


Для корректной работы адаптера необходимо расширить ViewController протоколом ListAdapterDataSource.

extension ViewController: ListAdapterDataSource {    func objects(for listAdapter: ListAdapter) -> [ListDiffable] {         return [            InfoSectionModel(companies: companies)        ]    }        func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {        let sectionController = InfoSectionController()        return sectionController    }        func emptyView(for listAdapter: ListAdapter) -> UIView? {        return nil    }}

Протокол реализует 3 метода:

func objects(for listAdapter: ListAdapter) -> [ListDiffable]


Метод требует вернуть массив заполненной модели для SectionController.

func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController


Этот метод инициализирует нужный нам SectionController.

func emptyView(for listAdapter: ListAdapter) -> UIView?


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

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

Обработка событий нажатия


Нам требуется расширить SectionController протоколом ListBindingSectionControllerSelectionDelegate и добавить в инициализаторе соответствие протоколу.

dataSource = selfextension InfoSectionController: ListBindingSectionControllerSelectionDelegate {    func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, didSelectItemAt index: Int, viewModel: Any) {        guard let cellModel = viewModel as? ExpandingCellModel        else {            return        }                delegate?.sectionControllerDidTapField(cellModel)    }}


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

func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, didSelectItemAt index: Int, viewModel: Any) 


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

protocol InfoSectionControllerDelegate: class {    func sectionControllerDidTapField(_ field: ExpandingCellModel)}

Мы расширим ViewController и теперь при нажатии на ячейку ExpandingCellModel в модели данных Company изменим свойство isOpened. Далее адаптер обновит состояние UICollectionView, и следующий метод из SectionController отрисует новую открывшуюся ячейку:

func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable] 

extension ViewController: InfoSectionControllerDelegate {    func sectionControllerDidTapField(_ field: ExpandingCellModel) {        guard let company = companies.first(where: { $0.id == field.identifier })        else { return }        company.isExpanded.toggle()                adapter.performUpdates(animated: true, completion: nil)    }}


Подводя итоги


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

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

Спасибо за внимание! Пример работы с фреймворком можно посмотреть в нашем репозитории.

Пример в gif

Подробнее..

Перевод С чего начать изучение Flutter в 2021 году

17.03.2021 10:08:48 | Автор: admin

Как и многие мобильные разработчики, мы с нетерпением ждали презентации Flutter и теперь хотим поделиться с читателями Хабра переводом статьи Tadas Petra о том, как можно выстроить свое обучение, если вы хотите познакомиться с Flutter и кроссплатформенными приложениями в 2021 году. Кстати, мы подключились к созданию курса Flutter, и об этом тоже расскажем в конце статьи. Приглашаем прочитать или посмотреть видеоверсию!

***

2021 год обещает быть очень важным для Flutter. Комьюнити разработчиков продолжает стремительно расти, а 3 марта 2021 года состоялась презентация Flutter Engage. Это делает потенциал Flutter поистине огромным.

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

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

Основы

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

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

Учебные ресурсы

Документация Flutter

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

Codelabs

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

YouTube

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

Мой канал

Robert Brunhage

Fun With Flutter

Resocoder

MTechViral

Fireship

FilledStacks

FlutterExplained

Официальный канал Flutter

freeCodeCamp

Free code camp это одна из самых больших площадок для онлайн-обучения программированию. Уверен, со временем у них будет появляться все больше нового контента. Сейчас у них есть несколько видеокурсов по Flutter, включая мой.

Учебные курсы

Одним из лучших курсов по Flutter я считаю курс от Angela Yu. Сам я его не проходил, но слышал очень лестные отзывы о нем. Он имеет рейтинг 4.7 и более 90000 студентов на Udemy, что говорит о его качестве. Помимо Flutter, в его программу входит также изучение темы ООП и его принципов: инкапсуляции, наследования и полиморфизма.

Awesome Flutter

Это отличный GitHub-репозиторий, который содержит огромное количество ресурсов по Flutter.

Присоединяйтесь к сообществу

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

Изучайте на практике

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

Вот несколько приложений, которые вы можете создать для практики:

  • Планировщик задач

  • Приложение-чат

  • Приложение интернет-магазина

Примечание: кстати, к этому списку можно добавить и другие идеи например, лента новостей, помощь в выборе сериалов или мониторинг криптовалют (15 проектов на Flutter с разбором для новичков и не только).

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

Хороших проектов всем! Пусть 2021 станет отличным годом для Flutter. Посмотреть другие полезные материалы @tadaspetra можно на всех платформах, включая YouTube.

Из нашей практики

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

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

Также в этом году поделимся опытом с начинающими специалистами. По приглашению онлайн-университета Skillbox мы приняли участие в разработке нового дополненного курса Flutter и профессии разработчика кроссплатформенных приложений. Для этого мы записали блок лекций по нативным модулям и популярным архитектурам. Надеемся, что курс будет полезен разработчикам, и желаем успехов будущим коллегам!

Подробнее..

То, чего нам так не хватало Render Effect в Android 12

21.05.2021 10:23:30 | Автор: admin

Иногда бывает нужно размыть задний план на экранах мобильного приложения, например в чате. Теперь это можно сделать всего парой строк кода. В Android 12 появился новый API Render Effect, который позволяет накладывать визуальные эффекты на Canvas или View. Этот API радует своей простотой и высокой скоростью отрисовки. Наибольший интерес представляет Render Effect дляразмытия (BlurEffect), но в этой статье мы затронем и остальные виды эффектов. Материал может быть полезен не только андроид-разработчикам, но и дизайнерам мобильных приложений.

Итак, каким образом можно размыть задний план при отображении диалога? Раньше в Андроиде для этого надо было отрисовать все вью с заднего плана на bitmap-е и затем размыть его с помощью RenderScript или OpenGL. Но это означает, что в проекте появится немало запутанного для прочтения кода, либо придется подключить стороннюю библиотеку для размытия. Плюс добавятся обработчики событий отрисовки и код для получения битмапа. К тому же по производительности эти решения могут не давать желаемого результата. При использовании некоторых популярных библиотек для размытия разработчики замечают лаги, например, если есть RecyclerView, который содержит много BlurView.

С помощью Render Effect мы можем всего парой строк кода реализовать блюр без лагов. Render Effect работает эффективно за счет того, что он использует Render Nodes (узлы отрисовки). Они образуют иерархию аппаратно ускоренной отрисовки, и эта отрисовка происходит с помощью GPU (графического процессора). Перерисовываться будут только те части UI, которые оказались в состоянии invalidated. Все вычисления для Render Effect выполняются в Render потоке и не блокируют UI.

У Render Effect очень простой API:

val renderNode = RenderNode("myRenderNode")renderNode.setPosition(0, 0, 50, 50)renderNode.setRenderEffect(renderEffect) canvas.drawRenderNode(renderNode)

Мы добавили эффект для RenderNode методом setRenderEffect(), и его отрисовка происходит в методе Canvas.drawRenderNode().

Можно применить Render Effect и к узлу отрисовки вьюшки:

imageView.setRenderEffect(renderEffect)

Приведем пример создания эффекта:

val blurEffect = RenderEffect.createBlurEffect(       20f, //radiusX       20f, //radiusY              Shader.TileMode.CLAMP)imageView.setRenderEffect(blurEffect)

Здесь мы создаем эффект размытия и применяем его к узлу отрисовки, привязанному к imageView. Задаем интенсивность размытия по оси X и оси Y (Значения 20f) . Последний аргумент Shader.TileMode определяет то, как будет выглядеть эффект на краях отрисовываемой области.

Если применить размытие к корневому layout-у, то будут размыты все вьюшки: и кнопка, и ползунки.

Варианты значений TileMode:

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

  • CLAMP. Если shader выходит за пределы границ, используется цвет пикселей на границе. Выглядит практически также как MIRROR, но возможно незначительное искажение формы объектов возле границы.

  • REPEAT. Изображение повторяется горизонтально и вертикально. На изображении заметно, как темная вода из нижней части изображения отразилась на верхней границе изображения.

  • DECAL (появился в API 31). Отрисовка shader-а только в пределах границ. Можно заметить, что на границе изображение стало чуть светлее.

Мы также можем применить размытие при создании тени. Конечно, можно просто создать тень с помощью свойства Elevation:

android:elevation="10dp"

Однако, в этом случае мы не сможем задавать направление, в котором должна отбрасываться тень, или ее цвет. Также тень для Elevation по умолчанию работает только с формами скругленного прямоугольника (круг и прямоугольник частные случаи прямоугольника со скругленными углами). Для тени другой формы нужно реализовать ViewOutlineProvider, чтобы он возвращал Outline с setPath(...), а это может быть весьма трудоемко.

Кастомную тень можно создать и с помощью xml, прописав <shape> с градиентом:

<gradient       android:type="radial"       android:centerColor="#90000000"       android:gradientRadius="70dp"       android:startColor="@android:color/white"       android:endColor="@android:color/transparent"/>

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

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

<shape android:shape="oval">   <solid android:color="#CCCCCC" /></shape>

Затем этот drawable устанавливаем в качестве содержимого ImageView и применяем размытие к ImageView:

imageView.setImageResource(R.drawable.gray_circle)val renderEffect = RenderEffect.createBlurEffect(20f, 20f, Shader.TileMode.CLAMP)imageView.setRenderEffect(renderEffect)

А вот как с помощью RenderEffect можно добиться тени произвольной формы и, если понадобится, произвольного цвета:

<ImageView   android:id="@+id/shadow"   android:layout_width="150dp"   android:layout_height="150dp"   android:src="@drawable/ic_baseline_time_to_leave_24"   android:tint="#444444"/>
val effect = RenderEffect.createBlurEffect(10f, 10f, Shader.TileMode.CLAMP)imageViewfindViewById<ImageView>(R.id.shadow).setRenderEffect(effect)

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

Всего есть семь видов RenderEffect:

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

  • Blur размытие по оси X и Y.

  • ColorFilter применяется цветной фильтр (см. скриншот ниже).

  • Offset сдвиг по осям X, Y (жаль, что нет поворота).

  • Shader отрисовывает шейдер, переданный в аргументах. С помощью шейдеров можно создавать, например, различные градиенты.

  • Chain комбинация 2 эффектов. Результат отрисовки одного эффекта используется как источник для второго эффекта.

  • BlendMode для объединения 2 эффектов в определенном режиме.

Можно добиться красивого эффекта замерзшего стекла (или запотевшего окна): он достигается сочетанием размытия и наложения прозрачно-белого цвета.

Применим такой эффект к панели внизу экрана. Фактически здесь надо сочетать три эффекта: отрисовка битмапа + размытие + цветной фильтр. К сожалению, нельзя сделать так, чтобы размывалась та часть UI, которая отрисована под вьюшкой. Поэтому RenderEffect нужно применить не к самой панели, а к тому, что находится под ней и содержит фоновое изображение: к imageView или к корневому layout-у. Комбинировать эффекты можно 2 способами: передавать один эффект в параметр create-метода другого эффекта, либо использовать метод createChainEffect():

val bmpEffect = RenderEffect.createBitmapEffect(...)val blurBmpEffect = RenderEffect.createBlurEffect(..., bmpEffect, ..)val finalEffect = RenderEffect.createColorFilterEffect(..., blurBitmapEffect)

или

val finalEffect = RenderEffect.createChainEffect(colorEffect,blurEffect)

Приведем пример наложения цветного фильтра:

val argb = Color.valueOf(red, green, blue, transparency).toArgb()val colorEffect = RenderEffect.createColorFilterEffect(       PorterDuffColorFilter(argb, PorterDuff.Mode.SRC_ATOP))

Результат:

Или можно с помощью цветного фильтра поменять насыщенность изображения:

val matrix = ColorMatrix()matrix.setSaturation(0f)imageView.setRenderEffect(RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix)))

Результат для setSaturation(0f) и setSaturation(100f):

BlendMode

С помощью метода RenderEffect.createBlendModeEffect() можно объединить два эффекта в одном из режимов:

  • CLEAR

  • SRC

  • DST

  • SRC_OVER

  • DST_OVER

  • SRC_IN

  • DST_IN

  • SRC_OUT

  • DST_OUT

  • SRC_ATOP

  • DST_ATOP

  • XOR

  • PLUS

  • MODULATE

  • SCREEN

  • OVERLAY

  • DARKEN

  • LIGHTEN

  • COLOR_DODGE

  • COLOR_BURN

  • HARD_LIGHT

  • SOFT_LIGHT

  • DIFFERENCE

  • EXCLUSION

  • MULTIPLY

  • HUE

  • SATURATION

  • COLOR

  • LUMINOSITY

Подробное описание режимов можно посмотреть здесь. Для примера приведем три режима (здесь источник, то есть первый RenderEffect это синий квадрат, а приемник, то есть второй Render Effect красный круг):

DST_ATOP выбрасывает пиксели, которые не накладываются на источник. DST_ATOP выбрасывает пиксели, которые не накладываются на источник. OVERLAY умножает цвета.OVERLAY умножает цвета.COLOR сохраняет оттенок и насыщенность источника, а яркость делает как у приемника.COLOR сохраняет оттенок и насыщенность источника, а яркость делает как у приемника.

Как уже упоминалось выше, RenderEffect работает крайне эффективно. При скролле RecyclerView с большим количеством элементов, у которых размыто по две ImageView (размытие с параметрами radiusX: 20f, radiusY: 20f, Shader.TileMode.CLAMP), никаких подтормаживаний не наблюдается. Размытие вью с анимацией тоже работает без задержек. В настоящий момент работа RenderEffect была проверена автором на эмуляторе Android 12 (API 31).

Что касается поддержки RenderEffect API, можно надеяться, что она будет добавлена в библиотеке AndroidX для Android 10 и 11, так как Render Nodes, лежащие в основе RenderEffect, были добавлены в Android 10 (API level 29). Однако, как пишут в официальной документации, Render Effect могут поддерживать не все устройства: Different Android devices may or may not support the feature due to limited processing power.

Отметим также, что Render Effect является одним из вариантов замены для RenderScript API, который устарел начиная с Android 12.

Заключение

Итак, в Android 12 мы получили то, чего так долго не хватало многим разработчикам простой API для отрисовки визуальных эффектов. Сочетая простые эффекты (размытие, цветные фильтры, шейдеры), мы можем получать интересные графические результаты. При этом больше не нужно возиться с вытаскиванием bitmap-изображения, эффект можно просто повесить на вьюшку. А благодаря использованию RenderNodes, отрисовка RenderEffect работает очень быстро.

Спасибо за внимание! Надеемся, что наш опыт был вам полезен.

Подробнее..

Удаленка по новым правилам 13 вопросов и ответов

20.04.2021 16:13:49 | Автор: admin

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

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

1) На какой максимальный срок могут принять или перевести на временную дистанционную работу?

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

2) Можно ли работать поочередно из дома и из офиса?

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

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

На сотрудников, которые впервые поступают на работу с 1 января 2021 года, работодатель сразу формирует электронные трудовые книжки, а трудовая книжка в бумажном виде не заводится (ст. 66.1 Трудового кодекса РФ).

При отсутствии у новичков СНИЛС тем, кто работает удаленно, придется самим оформлять этот документ, если трудовой договор с ними заключают путем обмена электронными документами (ст. 312.1 Трудового кодекса РФ).

4) Можно ли отправить подписанную скан-копию трудового договора работодателю по электронной почте?

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

Указанные выше правила относятся также к подписанию следующих документов:

  • дополнительные соглашения к трудовому договору;

  • договоры о материальной ответственности;

  • ученические договоры;

  • а также при внесении изменений в эти договоры и их расторжении (ст. 312.3 Трудового кодекса РФ).

Всеми остальными документами работник и работодатель могут обмениваться в иной форме, предусмотренной локальным нормативным актом, трудовым договором, дополнительным соглашением к трудовому договору (ст.312.3 Трудового кодекса РФ).

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

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

6) Вправе ли работодатель знакомить дистанционного работника с приказами, уведомлениями, требованиями и иными документами через электронную почту?

Да, вправе если подобное ознакомление и взаимодействие между работником и работодателем предусмотрено локальным нормативным актом, трудовым договором, дополнительным соглашением к трудовому договору (ст. 312.3 Трудового кодекса РФ).

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

Более того, если взаимодействие через электронную почту закреплено документально, то и дистанционный работник вправе обратиться к работодателю с заявлением, предоставить объяснения либо другую информацию по электронной почте (ст. 312.3 Трудового кодекса РФ).

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

7) Как направить в адрес работодателя больничный лист для получения по нему оплаты?

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

8) Может ли работодатель вызвать дистанционного сотрудника для работы в офисе?

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

9) Должен ли работодатель обеспечивать дистанционного работника компьютером/ноутбуком и другим оборудованием?

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

10) Если дистанционный работник привык к своему компьютеру/ноутбуку, может ли он работать на нем и отказаться от оборудования работодателя?

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

11) Может ли работодатель направить дистанционного работника в командировку?

Да, такая возможность предусмотрена с 01.01.2021 года (ст. 312.6 Трудового кодекса РФ).

12) По каким основаниям может быть уволен дистанционный работник по инициативе работодателя?

Помимо оснований, ранее уже предусмотренных Трудовым кодексом РФ (ст. 71 и 81), появились новые основания расторжения трудового договора с дистанционным работником (ст. 312.8 Трудового кодекса РФ):

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

  • Второе работник переехал и по объективным причинам не может выполнять работу так же эффективно, как раньше. Расстаться по этому основанию можно только с тем сотрудником, которому удаленку оформили на неопределенный срок.

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

13) Если сотрудник не является дистанционным работником, вправе ли работодатель перевести его на удаленную работу без его согласия?

Вправе в случае наличия следующих обстоятельств (ст. 312.9 Трудового кодекса РФ):

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

  • Второе в случае принятия соответствующего решения органом государственной власти и (или) органом местного самоуправления на период действия такого решения.

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

Подводя итоги

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

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

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

Подробнее..

Тестируем комплементарную кросс-энтропию в задачах классификации текста

27.11.2020 20:16:23 | Автор: admin
Ранее в этом году И. Ким совместно с соавторами опубликовали статью [1], в которой предложили новую функцию потерь для задач классификации. По оценке авторов, с её помощью можно улучшить качество моделей как в сбалансированных, так и в несбалансированных задачах классификации в сочетании со стандартной кросс-энтропией.

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

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



Предварительный анализ


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

В статье комплементарная кросс-энтропия определяется следующим образом:



а полная функция потерь, используемая в обучении модели это сумма со стандартной кросс-энтропией:



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

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

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

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

Проектирование эксперимента


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

Мы будем использовать простой классификационный датасет с Kaggle [2]. Он подразумевает задачу классификации тональности с пятью классами. Однако крайние классы (очень негативные и очень положительные) и их более умеренные аналоги обычно приписываются очень похожим текстам (см. рис. 1 для примера). Вероятно, это связано с определенной процедурой генерации этого набора данных.



Рис. 1. Примеры однотипных текстов, отнесенных к разным классам.

Чтобы упростить задачу, мы переназначим классы и сделаем три: отрицательный, нейтральный, положительный.

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

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



Таблица 1. Классовые пропорции, используемые в экспериментах. Коэффициенты даны относительно количества отрицательных примеров.

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

Наконец, мы разделили данные на train, validation и test сеты в пропорции 0.7 / 0.1 / 0.2.

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

Детали проекта


Исходный код нашей реализации, включая предварительную обработку данных, функцию потерь, модель и эксперименты, доступны на GitHub [3]. Здесь мы просто рассмотрим его ключевые моменты.

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

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

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

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

Детали процесса обучения: batch size 256, learning rate 3e-4, размер эмбеддингов 300, размер LSTM 128, уровень dropout 0,1, обучение в течение 50 эпох с остановкой после 5 эпох без улучшения качества на валидации. Мы используем одни и те же параметры как для экспериментов с комплементарной, так и для экспериментов со стандартной кросс-энтропией.

Результаты экспериментов





Таблица 2. Результаты экспериментов. CE для эксперимента со стандартной кросс-энтропией и ССЕ для комплементарной.

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

Ещё одну проблему комплементарной кросс-энтропии можно увидеть на графиках функции потерь (рис. 2).



Рис. 2. Графики потерь для степеней дисбаланса 1 / 0.2 / 0.2 (оранжевый) и 1 / 0.5 / 0.5 (зеленый).

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

В заключение


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

Примечания


[1] Y. Kim et al, 2020. Imbalanced Image Classification with Complement Cross Entropy. arxiv.org/abs/2009.02189
[2] www.kaggle.com/c/sentiment-analysis-on-movie-reviews/overview
[3] github.com/simbirsoft/cce-loss-text
Подробнее..

От тестировщика до QA. Как сократить путь в профессию на несколько месяцев

07.09.2020 16:04:06 | Автор: admin

В 2020 году в пятерке самых востребованных ИТ-профессий специалист по тестированию, или QA Engineer, по данным порталов для поиска работы. Рынок растет, и ИТ-компании активно формируют команды Quality Assurance.


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


Можно ли развить навыки быстрее, не теряя в качестве? Этот вопрос встал перед нами особенно остро, когда все наши ежегодные ИТ-митапы и интенсивы пришлось переносить в онлайн. Делимся мнением, что должно быть в программе, чтобы качественно и при этом быстро сделать первые шаги в QA в среднем за 3 месяца (или 60+ часов). Надеемся, что этот опыт пригодится всем, кто вовлечен в передачу знаний в QA, и ждем ваших откликов.



Всем привет! Меня зовут Марина, я руковожу QA-командой SimbirSoft в Саранске. В тестировании я с 2014 года пришла из другой отрасли на курс по тестированию и открыла для себя новую профессию. Занимаюсь тестированием веб- и мобильных приложений и обучением сначала работала с новичками, сейчас с группой QA Lead. Наша команда также готовит QA-специалистов к сертификации ISTQB.


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


Такие встречи мы начали проводить еще в 2012 году в нашем главном офисе в Ульяновске и на базе университетов. В 2019 году митапы и интенсивы проходили уже во всех наших центрах разработки в Казани, Самаре, Саранске, Димитровграде. Регистрацию мы проводим через TimePad и за год получаем более 2000 заявок на участие.


До 2020 года мы все делали оффлайн: приглашали участников в наши офисы, устраивали экскурсии. Сейчас, как и все, мы переехали в Zoom и YouTube.



Часть большой QA-команды SimbirSoft


Интенсивы онлайн: с чего мы начинали


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


Что было на интенсиве: 9 лекций, домашние работы, а также чат с преподавателями. Всего мы получили более 600 заявок и пригласили всех, кто уверенно справился с тестовым заданием. Убедились, что начинающие QA заинтересованы в том, чтобы перенять опыт у практиков.


Наши наблюдения по итогам интенсива:


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

Эти наблюдения легли в основу большого интенсива QA Skills, который стартует осенью. Рассказываем подробнее о программе.



Кому будет полезно


Наша QA-команда подготовила новую расширенную и сбалансированную программу для входа в профессию.


  • Для новичков в IT.
  • Для тестировщиков и QA-специалистов начального уровня.

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


Когда: с 1 октября 2020 года. Продолжительность составит около 2 месяцев.
Программа интенсива охватывает более 60+ часов. В них в том числе входят 19 онлайн-консультаций, задачи для самоконтроля и проектная работа в команде.


О чем мы расскажем


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


  1. Процессы QA в циклах разработки ПО.
  2. Требования и их анализ.
  3. Виды и уровни тестирования (часть I).
  4. Виды и уровни тестирования (часть II).
  5. Виды тестовой документации: test plan, стратегия тестирования, отчет по тестированию.
  6. Виды тестовой документации: test case, test suite, чек-лист, матрица трассировки, bug report.
  7. Техники тест-дизайна: черный ящик.
  8. Техники тест-дизайна: белый ящик.
  9. Клиент-серверная архитектура и особенности API.
  10. Тестирование API. REST.
  11. Тестирование API. SOAP UI.
  12. Особенности тестирования десктопных приложений.
  13. Особенности тестирования веб-приложений.
  14. Особенности тестирования мобильных приложений.
  15. Особенности операционных систем + виртуальные машины.
  16. Система контроля версий Git и работа с ним.
  17. SQL-запросы для QA.
  18. Автоматизация в тестировании. Selenium.
  19. Мастер-класс по прохождению интервью и составлению резюме.

Делимся впечатлениями



Мария, QA-специалист: Моим первым шагом в QA стал Летний интенсив SimbirSoft в 2019 году. Тогда у нас было несколько команд, и каждый выбирал для себя роль. Например, я выбрала QA и тестировала Оленеметр приложение для просмотра статистики в играх. Мы хорошо сработались с ребятами, было здорово влиться в команду и ощущать вовлеченность, а после интенсива нескольких из нас и меня в том числе пригласили на собеседование. Я пришла в QA из другой отрасли, так что при подготовке здорово волновалась до сих пор помню, как мурашки бегали!

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

Чему вы научитесь


  • Составите test plan, test case и отчет по результатам тестирования.
  • Проведете тестирование и поиск багов в условиях, приближенных к реальности, на специально созданном тестовом стенде.
  • Научитесь заводить задачи в наиболее распространенном таск-трекере Jira.
  • Познакомитесь с функциональным и нефункциональным тестированием.
  • Освоите инструменты тестирования REST и SOAP.
  • Составите простые запросы с оператором Select и др.
  • Примените на практике команды для работы с Git.
  • Проанализируете техническое задание на соответствие характеристикам требований.
  • Узнаете, какие тесты нужно автоматизировать и какие инструменты для этого пригодятся.
  • Получите базовые навыки работы с известными операционными системами mac, Linux, Windows.

О том, что мы подготовили в рамках интенсива QA Skills, рассказывает руководитель направлений QA Анастасия Леонтьева:



В чем плюсы


  • Поддержка менторов и их фидбек на протяжении всего интенсива: обратная связь от профессионала поможет найти точки роста и достичь качественных результатов.
  • Самоконтроль с поддержкой менторов.
  • Опыт работы над проектом в Agile-команде и пополнение портфолио.
  • Взаимодействие с участниками обмен опытом с комьюнити.
  • Мастер-класс по прохождению интервью и составлению резюме.
  • Возможность получить приглашение на собеседование в нашу QA-команду.

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


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


Приглашаем на интенсив QA Skills всех, кто хочет развивать навыки тестирования вместе с нами! Регистрация на TimePad


Зарегистрируйтесь, назовите нашему администратору кодовое слово "HABR" и получите скидку 25% на регистрацию до 15 сентября. Кроме того, всем желающим мы вышлем рекомендации наших практиков о том, как изучать QA самостоятельно. Ждем вас!

Подробнее..

Перевод Тестирование в Puppeteer vs Selenium vs Playwright сравнение производительности

29.01.2021 10:18:54 | Автор: admin

Ранее мы уже писали о том, когда бывает нужна автоматизация тестирования и какие проверки при этом используют. Сегодня предлагаем обсудить использование инструментов на практике и оценить их производительность. С разрешения Giovanni Rago автора серии полезных материалов о тестировании мы перевели его статью Puppeteer vs Selenium vs Playwright: сравнение скорости (Puppeteer vs Selenium vs Playwright, a speed comparison). Статья будет интересна тем, кто задумывается о выборе подходящего инструмента автоматизации в своих проектах.

От автора:

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

Задача определения наиболее быстрого инструмента автоматизации не так проста. Поэтому мы решили провести свой бенчмарк тест производительности, чтобы сравнить новичков Puppeteer и Playwright с ветераном WebDriverIO (в связке с Selenium и протоколом автоматизации DevTools).

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

Почему мы сравниваем эти инструменты автоматизации?

Рассматривать Puppeteer/Playwright и Selenium это всё равно что сравнивать яблоки с апельсинами: инструменты имеют существенно разные возможности и применяются в разных областях автоматизации, и тот, кто их оценивает, должен учитывать это, прежде чем анализировать скорость.

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

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

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

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

Методология, или как мы запускали тесты

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

Общие рекомендации

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

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

  2. Простое выполнение: скрипты запускались так, как это было показано в документации к каждому инструменту и с минимальными конфигурациями. Например, для Playwright node script.js

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

  4. Самые свежие версии: для тестирования всех инструментов мы использовали их последние версии.

  5. Одинаковый браузер: все скрипты выполнялись в headless Chromium.

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

Техническая настройка

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

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

Все тесты мы проводили на MacBook Pro 16 последнего поколения под управлением macOS Catalina 10.15.7 (19H2) со следующими характеристиками:

Модель: MacBookPro16,1

Процессор: 6-Core Intel Core i7

Скорость процессора: 2,6 ГГц

Количество процессоров: 1

Количество ядер: 6

Кэш-память L2 (на ядро): 256 Кб

Кэш-память L3: 12Мб

Технология Hyper-Threading: включена

Память: 16 Гб

Мы использовали следующие зависимости:

bench-wdio@1.0.0 /Users/ragog/repositories/benchmarks/scripts/wdio-selenium

@wdio/cli@6.9.1

@wdio/local-runner@6.9.1

@wdio/mocha-framework@6.8.0

@wdio/spec-reporter@6.8.1

@wdio/sync@6.10.0

chromedriver@87.0.0

selenium-standalone@6.22.1

scripts@1.0.0 /Users/ragog/repositories/benchmarks/scripts

playwright@1.6.2

puppeteer@5.5.0

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

Измерения

Мы получили следующие показатели, все рассчитано на основе 1000 прогонов:

* Среднее время выполнения (в секундах).

* Стандартное отклонение (в секундах): показатель разброса времени выполнения.

* Коэффициент вариации (CV): безразмерный коэффициент, который показывает отклонение результатов от среднего.

* P95 (изменение 95-го процентиля): наибольшее значение, оставшееся после отбрасывания верхних 5% отсортированного списка полученных данных. Интересно было бы узнать, как выглядит не экстремальное, но все еще высокое значение.

Что мы не измерили (пока)

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

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

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

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

Результаты

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

Запуск на демосайте

Первый бенчмарк запускался на нашем демонстрационном сайте. Эта веб-страница, размещенная на Heroku, создана на основес использованием Vue.js и использует бэкенд на Express. В большинстве случаев данные с бэкенда фактически не извлекаются. Вместо этого фронтенд хранит данные на стороне клиента.

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

Общие результаты выглядят следующим образом:

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

Первое, что обращает на себя внимание это большая разница между средним временем выполнения для Playwright и Puppeteer, причем последний почти на 30% быстрее и демонстрирует меньший разброс в его производительности. Мы задумались, не связано ли это с более длительным запуском со стороны Playwright. Мы не стали рассматривать этот и аналогичный вопросы, во избежание увеличения объема работ для первого бенчмарка.

Playwright vs PuppeteerPlaywright vs Puppeteer

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

WebDriverIO with WebDriver vs WebDriverIO with DevToolsWebDriverIO with WebDriver vs WebDriverIO with DevTools

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

Puppeteer vs WebDriverIO with DevToolsPuppeteer vs WebDriverIO with DevTools

Запуск на реальном веб-приложении

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

Запущенный нами скрипт очень похож на классический тест E2E: вход в Checkly, настроенная проверка API, сохранение и тут же удаление. Мы с нетерпением ждали этого сценария, но у каждого из нас были разные ожидания относительно того, как будут выглядеть цифры.

Результаты бенчмарка для нашего проверочного сценария ChecklyРезультаты бенчмарка для нашего проверочного сценария Checkly

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

Playwright vs PuppeteerPlaywright vs Puppeteer

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

Playwright vs WebDriverIO with SeleniumPlaywright vs WebDriverIO with Selenium

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

Теперь мы отступим назад и сравним время выполнения в разных сценариях:

Среднее время выполнения тестовых сценариевСреднее время выполнения тестовых сценариев

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

Заключение

Прежде всего, давайте рассмотрим инструменты от самых быстрых к самым медленным для обоих сценариев тестирования:

Рейтинг производительностиРейтинг производительности

Наше исследование производительности позволило сделать несколько интересных выводов:

  • Хотя Puppeteer и Playwright используют сходные API, похоже, что Puppeteer имеет значительное преимущество в скорости на более коротких скриптах (по нашим наблюдениям, выигрыш составляет около 30%).

  • Скрипты Puppeteer и Playwright показывают более быстрое время выполнения (около 20% в сценариях E2E) по сравнению с вариантами Selenium и DevTools WebDriverIO.

  • Протоколы автоматизации WebDriverIO, WebDriver и DevTools показали сопоставимое время выполнения.

Выводы

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

  • Имеет смысл подумать, необходим ли вам запуск установки большего количества barebone-систем. Возможно, что удобство дополнительных инструментов WebDriverIO стоит того, чтобы потратить немного больше времени на ожидание результатов.

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

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

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

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru