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

Код

Исправление кратных ошибок при кодировании сообщений

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

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


Теоретические положения


Идея использования организованной избыточности в сообщениях привела Р. Хемминга к построению корректирующего кода описанного здесь. Линейный корректирующий (n, k)-код характеризуется проверочной (mn) матрицей H. Требования к матрице просты: число строк совпадает с числом проверочных символов, ее столбцы должны быть отличны от нулевого и все различны. Более того, значения столбцов описывают номера позиций, занимаемых в кодовом слове символами слова, являющимися элементами конечного поля.

Часто для установления ошибочности переданного слова декодер использует вычисление синдрома, вычисляемого для этого слова. Синдром равен сумме столбцов этой матрицы, умноженных на компоненты вектора ошибки. Если в H имеется m строк и код позволяет исправлять одиночные ошибки, то длина блока (кодового слова) не превышает $ n2^m-1 $. Важна также выполнимость требуемой удаленности кодовых слов друг от друга.

Коды Хемминга достигают этой границы. Каждая позиция кодового слова кода Хемминга может быть занумерована двоичным вектором, совпадающим с соответствующим столбцом матрицы H. При этом синдром будет совпадать непосредственно с номером позиции, в которой произошла ошибка (если она только одна) или с двоичной суммой номеров (если ошибок несколько).
Идея векторной нумерации весьма плодотворна. Далее будем полагать $ n2^m-1 $ и, что i-я позиция слова занумерована числом i.

Нумерацию в двоичном виде,(т.е. такое представление) называют локатором позиции. Допустим, что требуется исправлять все двойные и одиночные ошибки. Видимо, для этого потребуется большая избыточность кода, т.е. матрица H должна иметь больше строк (вдвое большое число). Поэтому будем формировать матрицу H с 2m строками и с $ n2^m-1 $ столбцами, и эти столбцы разумно выбирать различными. В качестве первых m строк будем брать прежнюю матрицу кода Хемминга. Это базисные векторы-слова пространства слов.

Пример 1. Пусть m = 5 и n = 31. Желательно было бы получить (n, k)-код, исправляющий двойные ошибки, с проверочной матрицей Н в виде:

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

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

Пример 2 .
00000 0
00010 1
00011 х

$11011 х^4 +х^3+х+1$
Сумма и разность таких многочленов соответствует сумме и разности векторов:
0 0 = 0, 0 1 = 1, 1 0 = 1, 1 1 = 0, знаки имеют в двоичном случае совпадающий смысл. Не так с умножением, показатель степени результата умножения может превысить 4.

Пример 3 .
$(х^3+х+1)х^4+х^3+х+1)=х^7+х^6+х^5+3х^4+2х^3+х^2+2х+1=$
$=х^7+х^6+х^5+х^4+х^2+1$.

Необходим метод понижения степеней больше 4.
Он называется (редукцией) построением вычетов по модулю неприводимого многочлена M(x) степени 5; метод состоит в переходе от многочленов произведений к их остаткам от деления на $M(x)=x^5+x^2 + 1$


Так что
$х^7+х^6+х^5+х^4+х^2+1 =(х^2+х + 1)(х^5 +х^2 +1) +х^3 +х^2 +х$ или $х^7+х^6+х^5+х^4+х^2+1 (х^3+х^2 + x)mod(х^5 +х^2 +1).$

Символ читается сравнимо с.
В общем виде A(x) a(x)mod M(x)
Тогда и только тогда, когда существует такой многочлен C(x), что
A(x)= M(x)C(x) +a(x) коэффициенты многочленов приводятся по модулю два:
A(x) a(x)mod(2,M(x)).

Важные свойства сравнений


Если а(x) А(x)mod M(x) и b(x) B(x)mod M(x), то
а(x) b(x) А(x) B(x)mod M(x) и
а(x)b(x) А(x)B(x)mod M(x).

Более того, если степени многочленов а(х) и А(х) меньше степени М(х), то из формулы
а(x) А(x)mod(2,M(x)) следует, что а(x) = А(x).
Различных классов вычетов существует 2 в степени degM(x) т.е. столько, сколько существует различных многочленов степени, меньшей m, т.е. сколько может быть различных остатков при делении. С делением еще больше сложностей.

Алгоритм деления


Для чисел.
Для данных a и M существуют однозначно определенные числа q и A, такие, что а =qM + A, 0 A M,
Для многочленов с коэффициентами из данного поля.
Для данных a(x) и M(x) существуют однозначно определенные многочлены q(x) и A(x), такие, что a(x) = q(x)M(x) + A(x), degA(x) <deg M(x).
Возможность деления многочленов обеспечивается алгоритмом Евклида.

Для чисел пример расширенного НОД описан здесь
Для заданных a и b существуют такие числа A и B, что aA +bB = (a,b), где (a,b) НОД чисел a и b.
Для многочленов с коэффициентами из данного поля.
Для заданных a(x) и b(x) существуют такие многочлены A(x) и B(x), что
a(x)A(x) + b(x)B(x) = (a(x), b(x)),
где (a(x), b(x)) нормированный общий делитель a(x) и b(x) наибольшей степени.
Если а(х) и М(х) имеют общий делитель d(x) 1, то деление на a(x) по mod M(x) не всегда возможно.

Очевидно, что деление на a(x) эквивалентно умножению на A(x).
Так как если (a(x), b(x))= 1 =НОД, то согласно алгоритму Евклида, существуют такие A(x) и B(x), что a(x)A(x) + b(x)B(x) = 1, так, что a(x)A(x) 1mod b(x). Проверка того, что двоичный многочлен является неприводимым над полем GF ($2^5$), выполняется непосредственным делением на всевозможные делители со степенями, меньшими, чем deg M(x).

Пример 4. $M(x) = x^5+x^2 +1$ делим на х и на (х + 1)
на линейные делители. Результат деления не нуль. Делим на квадратные делители $х^2 , х^2 +х=х(х +1),х^2 +1, х^2 +2х +1= (х +1)^2, х^2 + х + 1.$. Они выдают остатки, не равные нулю. Делителей степени 3 не существует, так как их произведение дает степень 6.
Таким образом, многочлены можно складывать, вычитать, умножать и делить по модулю $M(x) = x^5+x^2 +1$.

Переходим к поиску функции для проверочной матрицы H, задающей код с исправлением двойных ошибок с блоковой длиной 31 и скоростью 21/31; 31-21=10 =2t проверочных символов = 10. Такая функция должна иметь своими корнями номера ошибочных позиций в кодовом слове, т.е. при подстановке в эту функцию номеров позиций, обращает ее в нуль.

Поиск функции


Предположим, что 1 и 2 номера искаженных символов (позиций) слова. Используя двоичную запись чисел 1 и 2 можно представить эти номера в виде классов вычетов по модулю M(x) т.е. установить соответствие i (i)(x) двоичные многочлены степени < 5.

Первые 5 проверочных условий определяют 1 + 2; второе множество проверочных уравнений должны определять f(1) + f(2).
Декодер должен определить 1 и 2 по заданной системе:

Какой же должна быть функция f(x)?

Простейшая функция это умножение на константу f() (х)modM(x).
Но тогда 2 = 1, т.е. уравнения системы зависимы. Новая пятерка проверочных условий декодеру не даст ничего нового.
Аналогично и функция f() = + не изменяет ситуацию, так как 2 = 1.
Пробуем степенные функции: сначала возьмем $f() = ^2$. При этом


Эти уравнения также зависимы, так как

$$display$$1^2 =(1 + 2)^2=1^2 + 212 + 2^2 = 1^2 + 2^2 =2$$display$$


Таким образом, второе уравнение является квадратом первого.
Пробуем $f() = ^3$. Уравнения декодера меняют вид:
Откуда $inline$2 =1^3 + 2^3=(1 + 2)(1^2 -12+ 2^2)=1(12-1^2).$inline$

Так что при 10 имеем

Значит, 1 и 2 удовлетворяют уравнению

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

Так как в поле двоичных многочленов по модулю M(x) данное уравнение имеет точно 2 корня, то декодер всегда сможет найти два нужных локатора.
Если произошла только одна ошибка, то 1=1 и $1^2 = 2$. Следовательно, в этом случае единственная ошибка удовлетворяет уравнению + 1 = 0 или 1+ 1-1= 0.

Наконец, декодер всегда производит декодирование, если ошибок не произошло, то в этом случае 1 + 2 = 0 .
Более удобно (на практике) оперировать не непосредственно с многочленом, корнями которого являются локаторы ошибок, а с многочленом, корни которого взаимны к локаторам; т.е. являются к ним мультипликативным обратными величинами.
Ясно, что при не более чем двух ошибках декодер может определить номера ошибок. Если же искажаются три или более символов, то произойдет ошибка декодирования или отказ от декодирования.
Таким образом, функция $f(x) = x^3$ подходит для построения нижних пяти строк проверочной матрицы Н двоичного кода с длиной кодовых слов 31 и 10-ю проверочными символами, исправляющего все двойные ошибки.
Первые пять проверок задают сумму номеров ошибок (S1); вторые пять проверок задают сумму кубов номеров ошибок (S3).
Процедура декодирования состоит из трех основных шагов:
1. каждое полученное кодовое слово проверяется и вычисляются S1 и S3;
2. находится многочлен локаторов ошибок от (z);
3. вычисляются взаимные величины для корней (z) и изменяются символы в соответствующих позициях полученного слова.
Подробнее..

Код Рида-Соломона

10.09.2020 12:19:55 | Автор: admin

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

Так, например, для определенного Рида-Соломона кода (РС-кода) необходимо установить:
длину n кодового слова (блока);
количество k информационных и N-k проверочных символов;
неприводимый многочлен р(х), задающий конечное поле GF(2r);
примитивный элемент конечного поля;
порождающий многочлен g(x);
параметр j кода;
используемое перемежение;
последовательность передачи кодовых слов или символов в канал и еще некоторые другие.

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

Описание РС-кода и его характеристик


Для удобства и лучшего уяснения сущности устройства РС-кода и процесса кодирования вначале приведем основные понятия и термины (элементы) кода.
Рида Соломона коды (РС-код) можно интерпретировать как недвоичные коды БЧХ (Боуза Чоудхури Хоквингема), значения кодовых символов которых взяты из поля GF(2r), т. е. r информационных символов отображаются отдельным элементом поля. Коды Рида Соломона это линейные недвоичные систематические циклические коды, символы которых представляют собой r-битовые последовательности, где r целое положительное число, большее 1.

Коды Рида Соломона (n, k) определены на r-битовых символах при всех n и k, для которых:
0 < k < n < 2r + 2, где
k число информационных символов, подлежащих кодированию,
n число кодовых символов в кодируемом блоке.

Для большинства (n, k)-кодов Рида Соломона; (n, k) = (2r1, 2r12t), где
t количество ошибочных символов, которые может исправить код, а
nk = 2t число контрольных символов.

Код Рида Соломона обладает наибольшим минимальным расстоянием (числом символов, которыми отличаются последовательности), возможным для линейного кода. Для кодов Рида Соломона минимальное расстояние определяется следующим образом: dmin = nk +1.

Определение. РС-кодом над полем GF(q=рm), с длиной блока n = qm-1, исправляющим t ошибок, является множество всех кодовых слов u(n) над GF(q), для которых 2t последовательных компонентов спектра с номерами $m_0,m_0 +1,...,m_0+2t-1$ равны 0.

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

Информационный многочлен Q. Задает текст сообщения, которое делится на блоки (слова) постоянной длины и оцифровывается. Это то, что подлежит передаче в системе связи.
Порождающий многочлен g(x) РС-кода многочлен, который преобразует информационные многочлены (сообщения) в кодовые слова путем перемножения Qg(x)= С =u(n) над GF(q).

Проверочный многочлен h(x) позволяет устанавливать наличие искаженных символов в слове.
Синдромный многочлен S(z). Многочлен, содержащий компоненты соответствующие ошибочным позициям. Вычисляется для каждого принятого декодером слова.
Многочлен ошибок E. Многочлен с длиной равной кодовому слову, с нулевыми значениями во всех позициях, кроме тех, что содержат искажения символов кодового слова.

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

Неприводимый многочлен поля р(x). Конечные поля существуют не при любом числе элементов, а только в случае, если число элементов является простым числом р или степенью q=рm простого числа. В первом случае поле называется простым (его элементы-вычеты чисел по модулю простого числа р), во втором-расширением соответствующего простого поля (его q элементов-многочленов степени m-1 и менее это вычеты многочленов по модулю неприводимого над простым полем многочлена р(x) степени m)

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

Таблица П Характеристики элементов конечного поля расширения GF(24), неприводимый многочлен p(x) = x4+x+1, примитивный элемент =0010= 210


Пример 1. Над конечным полем GF(24), задан неприводимый многочлен поля p(x) = x4 + x + 1, примитивный элемент =2, и задан (n, k)- код Рида-Соломона (РС-код). Кодовое расстояние этого кода равно d = n k + 1 = 7. Такой код может исправлять до трёх ошибок в блоке (кодовом слове) сообщения.

Порождающий многочлен g(z) кода имеет степень m =n-k = 15-9 = 6 (его корнями являются 6 элементов поля GF(24) в десятичном представлении, а именно элементы 2, 3, 4, 5, 6, 7) и определяется соотношением, т.е. многочленом от z с коэффициентами (элементами) из GF(24) в десятичном представлении при i = 1(1)6. В рассматриваемом РС-коде 29 = 512 кодовых слов.

Кодирование сообщений РС-кодом


В таблице П эти корни имеют и степенное представление $^1=2, ^2=3,...,^6=7$.
.
Здесь z- абстрактная переменная, а -примитивный элемент поля, через его степени выражены все (16) элементы поля. Многочленное представление элементов поля использует переменную х.
Вычисление порождающего многочлена g(x)=АВ РС-кода выполним частями (по три скобки):

Векторное представление (через коэффициенты g(z) элементами поля в десятичном представлении) порождающего многочлена имеет вид
g(z) = G<7>= (1, 11, 15, 5, 7, 10, 7).

После формирования порождающего многочлена РС-кода, ориентированного на обнаружение и исправление ошибок, задается сообщение. Сообщение представляется в цифровом виде (например, ASCII- кодом), от которого переходят к многочленному или векторному представлению.

Информационный вектор (слово сообщения) имеет k компонентов из (n, k). В примере k = 9, вектор получается 9-компонентный, все компоненты это элементы поля GF(24) в десятичном представлении Q<9> = (11, 13, 9, 6, 7, 15, 14, 12, 10).

Из этого вектора формируется кодовое слово u<15> вектор с 15 компонентами. Кодовые слова, как и сами коды, бывают систематическими и несистематическими. Несистематическое кодовое слово получают умножением информационного вектора Q на вектор, соответствующий порождающему многочлену
.

После преобразований получаем несистематическое кодовое слово (вектор) в виде
Qg = <11, 15, 3, 9, 6, 14, 7, 5, 12, 15, 14, 3, 3, 7, 1>.
При систематическом кодировании сообщение (информационный вектор) представляют многочленом Q(z) в форме Q(z)=q(z)g(z) + R(z), где степень degR(z)<m = 6. После этого к вектору Q справа приписывается остаток R (всё в десятичном виде). Это делается так.

Многочлен Q сдвигают в сторону старших разрядов на величину m = n k, что достигается путём умножения Q(z) на Zn k (в примере Zn k = Z 6) и выполняют после сдвига деление Q(z)Zn k на g(z). В результате находят остаток от деления R(z). Все операции выполняют над полем GF(24)
(11, 13, 9, 6, 7, 15, 14, 12, 10, 0, 0, 0, 0, 0, 0) =
=(1, 11, 15, 5, 7, 10, 7)(11, 15, 9, 10,12,10,10,10, 3) + (1, 2, 3, 7, 13, 9) = GS + R.

Остаток при делении многочленов вычисляется обычным способом (уголком см.здесь Пример 6). Деление выполняется по образцу: Пусть Q = 26, g(z) = 7 тогда 26 = 73 +R(z), R(z)=26 -73 =26-21 = 5. Вычисление остатка R(z) от деления многочленов. Приписываем к вектору Q справа остаток R.

Получаем u<15> кодовое слово в систематическом виде. Этот вид явно содержит информационное сообщение в k старших разрядах кодового слова
u<15> = (11,13,9,6,7,15,14,12,10; 1, 2, 3, 7, 13, 9).
Разряды вектора нумеруются справа налево от 0(1)14. Шесть младших разрядов справа являются проверочными.

Декодирование кодов Рида-Соломона


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

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

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

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

Обнаружение искажений.


Синдромный $S = (S_v,S_{v+1},...,S_{m+v-1})$, где $v{0, 1}$ вектор последовательно определяется для каждого из полученных декодером на его входе кодовых слов. При нулевых значениях компонентов вектора синдрома $Sj = 0, j=v,v+1,...,m + v - 1$, декодер считает, что в принятом слове ошибки нет. Если же хотя бы для одного $j1,Sj0$, то декодер делает вывод о наличии ошибок в кодовом векторе и приступает к их выявлению, что является 1-м шагом работы декодера.

Вычисление синдромного многочлена
Умножение на приемной стороне кодового слова С на проверочную матрицу Н может давать в результате два исхода:
синдромный вектор S=0, что соответствует отсутствию ошибок в векторе C;
синдромный вектор S0, что означает наличие ошибок (одной или более) в компонентах вектора C.

Интерес представляет второй случай.
Кодовый вектор с ошибками представлен в виде C(E) =C + E, E вектор ошибок. Тогда $(C +E)H^t = CH^t + EH^t = 0 + EH^t = S$
Компоненты Sj синдрома определяются либо соотношением суммирования
для n = q-1 и j = 1(1)m = n-k, либо схемой Горнера:
$inline$S_j = C_0 +^j(C_1 +^j(C_2 +...+^j(C_{n-2} +^jC_{n-1})...))$inline$

Пример 2. Пусть вектор ошибок имеет вид Е =<0 0 0 0 12 0 0 0 0 0 0 8 0 0 0>. Он искажает в кодовом векторе символы на 3-й и 10-й позициях. Значения ошибок соответственно 8 и 12 эти значения также являются элементами поля GF(24) и заданы в десятичном (табл. П) представлении. В векторе Е нумерация позиций от младших справа налево, начиная с 0(1)14.

Сформируем теперь кодовый вектор с двумя ошибками в 3-ем разряде и в 10-ом со значениями 8 и 12 соответственно. Это осуществляется суммированием в поле GF(24) по правилам арифметики этого поля. Суммирование элементов поля с нулем не изменяет их значения. Ненулевые значения (элементы поля) суммируются после преобразования их к многочленному представлению, как обычно суммируются многочлены, но коэффициенты при неизвестной приводятся по mod 2.

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

Ниже показано вычисление искажённых ошибками значений в 10 и 3 позициях кодового слова:
$inline$(7+12) ^6+^11 =x^3 +x^2 +x^3 +x^2 +x^1 =^1 = 2,$inline$
$inline$(3 + 8) ^2+ ^7 =x^2 +x^3 +x^1 + 1 =^{12}=13.$inline$

Декодер вычисления выполняет по общей формуле для компонентов Sj, j=1(1)m. Здесь (в модели) используем соотношение $S=EH^t$, так как E задаём (моделируем) в программе сами, то ненулевые слагаемые получаются только при i = 3 и i = 10.


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

Проверочная матрица РС кода


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

Сама матрица формируется специальным образом. Первые две строки очевидны, третья строка и все последующие получены вычитанием из предыдущей (второй) строки отрезка чисел натурального ряда 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 по mod 15. При возникновении нулевого значения оно заменяется числом 15, отрицательные вычеты преобразуются в положительные.


Каждая матрица соответствует своему порождающему многочлену для систематического и несистематического кодирования.

Определение коэффициентов синдромного многочлена


Далее будем определять коэффициенты синдромного многочлена при j=1(1)6.
Относительно кодового слова с длиной $n<q=p^r$, поступающего на вход декодера мы допускаем, что оно искажено ошибками.

Относительно вектора ошибок для его выявления необходимо знать следующее:
количество искаженных позиций у кодового слова
$vv_{max}=0.5m$;
номера (положение) искаженных позиций в кодовом слове $ _i: _i=0(1)n-1$;
значения (величины) искажений $inline$e_; e_GF(2^4)$inline$.
Как вычисляется и используется далее синдромный вектор (многочлен) S? Его роль при декодировании кодовых слов очень значительна. Покажем это с иллюстрацией на числовом примере.

Пример 3. (Вычисление компонентов синдромного вектора $S_{<6>}$ )

то в итоге имеем $S_{<6>}=(S_1,S_2,S_3,S_4,S_5,S_6)$ =<8,13,7,13,15,15>.

Для дальнейшего рассмотрения введем новые понятия. Величину $x_i = ^{_i}$будем называть локатором ошибок, здесь искаженный символ кодового слова на позиции $_i$, примитивный элемент поля GF(24).
Множество локаторов ошибок конкретного кодового слова рассматривается далее как коэффициенты многочлена локаторов ошибок (z), корнями $z_i$ которого являются значения $x_i ^{-1}$, обратные локаторам.

При этом выражения $(1-zx_i)=0 $обращаются в нуль.
$inline$(z) = (1-zx_1)(1-zx_2)...(1-zx_v) =_vz^v +_{v-1}z^{v-1} +...+_1z +_0$inline$
всегда свободный член уравнения всегда свободный член уравнения $_0 =1$.
Степень многочлена локаторов ошибок равна v количеству ошибок и не превышает величины $vv_{max}=0.5m$.

Все искаженные символы находятся на разных позициях слова, следовательно, среди локаторов $x_i = ^i$,, не может быть повторяющихся элементов поля, а многочлен (z)=0 не имеет кратных корней.
Величины ошибок для удобства записи переобозначим символом $Y_i = e_i$. Для коэффициентов синдромного многочлена ранее рассматривались нелинейные уравнения. В нашем случае v=1 начало отсчета компонентов синдрома.


где $y_i,x_i$ неизвестные величины, а $S_j$ известные, вычисляемые на первом этапе декодирования, параметры (компоненты синдромного вектора).
Методы решения подобных систем нелинейных уравнений неизвестны, но решения отыскивают, используя ухищрения (обходные пути). Выполняется переход к Ганкелевой (теплицевой) системе линейных уравнений относительно коэффициентов $_i$ многочлена локаторов.

Преобразование к системе линейных уравнений
В уравнение $_i$ многочлена локаторов ошибок подставляется значение его корней $z =x_i^{-1}$. При этом многочлен обращается в нуль. Образуется тождество, обе части которого умножаем на $y_ix_i^{j+v}$, получаем:
$inline$y_i(_vx_i^{j}+_{v-1}x_i^{j+1}+...+_1x_i^{j+v-1}+x_i^{j+v})=0,1iv, 1jv$inline$.

Таких равенств получаем $vv =v^2$.
Суммируем эти равенства по всем $ i, 1iv$, при которых эти равенства выполняются. Так как многочлен (z) имеет v корней $x_i^{-1}$, раскроем скобки и перенесем коэффициенты $_i$ за знак суммы:


В этом равенстве согласно системе нелинейных уравнений, приведенной
ранее, каждая сумма равна одному из компонентов вектора синдрома. Отсюда заключает, что относительно коэффициентов $_v, _{v-1},...,_1$ можно выписать систему уже линейных уравнений.


Знаки при вычислениях над двоичным полем опускаются, так как со-ответствуют +. Полученная система линейных уравнений является ганкелевой и ей соответствует матрица с размерами $v(v+1)$ бит.

Эта матрица не вырождена, если число ошибок в кодовом слове C(E) строго равно $v , v 0.5(n-k)$, т.е. способность помехоустойчивости данного кода не нарушилась.

Решение системы линейных уравнений


Полученная система линейных уравнений в качестве неизвестных содержит коэффициенты $_i =1(1)t$ многочлена локаторов ошибок для кодового слова C(E). Известными считаются вычисленные ранее компоненты синдромного вектора $S_j, j=1(1)m$. Здесь t количество ошибок в слове, m количество проверочных позиций в слове.
Существуют разные методы решения сформированной системы.

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

Над бесконечными полями известны методы решения ганкелевой системы линейных уравнений:
итеративный метод Тренча Берлекэмпа Месси (ТБМ-метод); (1)
прямой детерминированный Питерсона Горенштейна Цирлера; (ПГЦ метод); (2)
метод Сугиямы, использующий алгоритм Евклида для нахождения НОД (С-метод).(3)
Не рассматривая других методов, остановим свой выбор на ТБМ-методе. Мотивировка выбора следующая.

Метод (ПГЦ) прост и хорош, но для малого количества исправляемых ошибок, С-метод сложен для реализации на ЭВМ и ограниченно опубликован (освещен) в источниках, хотя С-метод как и ТБМ-метод по известному многочлену синдромов S(z) обеспечивает решение уравнения Падэ над полем Галуа. Это уравнение сформировано для многочлена локаторов ошибок (z) и многочлена (z), в теории кодирования называется ключевым уравнением Падэ:
$S(z)(z) = (z)(mod z^m)$.

Решением ключевого уравнения является совокупность $x_i^{-1}$ корней многочлена (z), и соответственно локаторов $x_i =^{_i}$, т.е. позиции ошибок. Значения (величины) ошибок $e_i$ определяются из формулы Форни в виде

где $_z^{'}(^{-i})$ и $ (^{-i})$ значения многочленов (z) и (z) в точке $z =^{-i}$, обратной корню многочлена (z);
i позиция ошибки;$_z^{'}(z)$ формальная производная многочлена (z) по z;

Формальная производная многочлена в конечном поле


Имеются различия и сходство для производной по переменной в поле вещественных чисел и формальной производной в конечном поле. Рассмотрим многочлен

$a_i$ это элементы поля, i = 1(1)n.
Элементы поля. Задан код над вещественным полем GF(24). Производная по z имеет вид: .
В бесконечном вещественном поле операции умножить на n и суммировать n раз совпадают. Для конечных полей производная определяется иначе.
Производная по аналогии определяется соотношением:

где ((i)) = 1+1+...+1, (i) раз, суммируемых по правилам конечного поля: знак + обозначает операцию суммировать столько-то раз, т.е. элемент $a_2z$ повторить 2 раза, элемент $a_3z^2$ повторить 3 раза, элемент $a_nz^{n-1}$ повторить n раз.

Ясно, что эта операция не совпадает с операции умножения в конечном поле. В частности, в полях GF(2r) сумма четного числа одинаковых слагаемых берется по mod2 и обнуляется, а нечетного равна самому слагаемому без изменений. Следовательно, в поле GF(2r) производная получает вид

вторая и старшие четные производные в этом поле равны нулю.

Из алгебры известно, если многочлен имеет кратные корни (кратность р ), то производная многочлена будет иметь этот же корень, но с кратностью р-1. Если р = 1, то f(z) и f '(z) не имеет общего корня. Следовательно, если многочлен и его производная имеют общий делитель, то существует кратный корень. Все корни производной f '(z) эти корни кратные в f(z).

Метод решения ключевого уравнения


ТМБ (Тренча-Берлекэмпа-Месси) метод решения ключевого уравнения.
Итеративный алгоритм обеспечивает определение многочленов (z) и (z), и решение уравнения Падэ (ключевого).

Исходные данные: коэффициенты многочлена $S_1,S_2,...,S_n$ степени n-1.
Цель. Определение в явном (аналитическом) виде многочленов (z) и (z).
В алгоритме используются обозначения: j номер шага, $v_j$ степень многочлена, $inline$_j(z) =_{ji}z^i +_{ji-1}z^{i-1}+...+_{j1}z+_{j0}$inline$ разложение многочлена по степеням $z, k_j, L_j, _j(z)$ и $ _j(z)$ промежуточные переменные и функции на j-м шаге алгоритма;

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


Пример 4. Выполнение итеративного алгоритма для вектора
S=(8,13,7,13,15,15). Определяются многочлены $(z) =_n(z)$ и $(z) = _n(z)$.
Таблица 2 Расчет многочленов локаторов ошибок




Итак $inline$_j^(z)=14z^2+13z+1$inline$, $inline$_j^(z)$inline$=7z+8.
Многочлен локаторов ошибок (z) над полем GF(24) с неприводимым многочленом p(x) = x4 + x + 1 имеет корни
$z_1 = ^{-i_1} = 13 = 4^{-1}$ и $z_2 =^{-i_2} = 6 = 11^{-1}$, в этом легко убедиться непосредственной проверкой, т.е. $inline$i_1= 3, i_2 =10, 13 = ^{12}, 1 =^{12}^{3}$inline$ и $^{12} =^{-3}=>13=4^{-1}$. Подстановка корней в
$inline$(z=13)=14(13)^2+1313+1=^{13}(^{12})^2+(^{12})^2+^0= ^{37}+^{24} +^{0}$inline$=
=$^{7}+^{9}+^0 =x^3+x+1=0(mod2)$;
$inline$(z = 6)=14(6)^2+136+1 = ^{13}(^{5})^2+(^{5})^2+^{0}$inline$=
=$^{8}+^{2} +^{0} = x^2 +1+x^2 +1 = 0(mod2)$.

Взяв формальную производную от (z), получаем _2(z) =214+13 =13, так как 14z берется в сумме 2 раза и по mod 2 обращается в нуль.
С использованием формулы Форни найдем выражения для расчета величин ошибок $e_i$.


Подстановкой значений i = 3 и i = 10 позиций в последнее выражение
находим
$е_3 = 10^{15-3}+11 =^{6}+^{10}$= =$x^3+x^2+x^2+x+1=x^3+x+1=^{7}=>8$;
$inline$е_{10} = 10^{15-10}+11 =^{9}^{5}+^{10}=^{14}+^{10}$inline$= =$x^3+x^2+x=^{11}=>12$.

Архитектура построения программного комплекса


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

Схема функционирования программного комплекса

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

Описание работы программного комплекса

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

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


Рисунок 3 Промежуточное представление результатов работы
программного комплекса

Рисунок 4. Результаты загрузки файла сообщения

Рисунок 5. Результаты кодирования файла

Рисунок 6. Вывод сообщения с внесенными в него ошибками.

Рисунок 7. Вывод результатов декодирования и сообщения с внесенными в него ошибками

Рисунок 8. Вывод декодированного сообщения.

Заключение


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

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

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

Список используемой литературы


1. Блейхут Р. Теория и практика кодов, контролирующих ошибки. М.: Мир, 1986. 576 с.
2. Мак-Вильямс Ф. Дж, Слоэн Н. Дж. А. Теория кодов, исправляющих ошибки. М.: Связь, 1979. 744 с.
3. Берлекэмп Э. Алгебраическая теория кодирования. М.: Мир, 1971. 478 с.
4. Габидулин Э.М., Афанасьев В.Б. Кодирование в радиоэлектронике. М.: Радио и связь, 1986. 176 с., ил.
5. Вернер М. Основы кодирования. Учебник для ВУЗов. М.: Техносфера, 2004. 288 с.
6. Трифонов П.В. Адаптивное кодирование в многочастотных системах. Диссертация на соискание ученой степени кандидата технических наук. СПб: Санкт-Петербургский государственный политехнический университет, 2005. 147 с.
7. Фомичев С. М., Абилов А.В. Обзор математических моделей каналов связи и их применение в телекоммуникационных системах. Ижевск: Ижевский государственный технический университет, 2001. 60 с.
8. Касами Т., Токура Н., Ивадари Е., Инагаки Я. Теория кодирования. М.: Мир, 1978. 576 с.
9. Муттер В. М. Основы помехоустойчивой телепередачи информации. Л.: Энергоатомиздат. Ленинградское отделение, 1990. 288 с.
10. Ваулин А. Е., Смирнов С.И. Моделирование помехозащищенного канала передачи сообщения в системе связи/Сборник алгоритмов и программ типовых задач. Вып.26. под редакцией ктн доц. И.А. Кудряшова . СПб.: ВКА им А.Ф. Можайского, 2007. стр. 121-130.
11. Карпушев С.И Конспект лекций по алгебре (часть 2. Абстрактная
алгебра). ВИКУ им. А. Ф. Можайского, 2002. 97 с.
12. Зайцев И. Е. Методика определения параметров помехоустойчивого каскадного кодирования. Л.: ВИКИ, 1987 120 с.
Подробнее..

Обработка дат притягивает ошибки или 77 дефектов в Qt 6

16.02.2021 22:10:00 | Автор: admin

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


Это классическая статья о проверке открытого проекта, которая пополнит нашу "доказательную базу" полезности и эффективности использования PVS-Studio для контроля качества кода. Хотя мы уже писали про поверку проекта Qt (в 2011, в 2014 и в 2018), очень полезно сделать это вновь. Так, мы на практике демонстрируем простую, но очень важную мысль: статический анализ должен применяться регулярно!


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


Всё, теория заканчивается. Давайте посмотрим, что интересного нас ждёт в коде. А пока вы будете читать статью, предлагаю скачать PVS-Studio и запросить демонстрационный ключ, чтобы посмотреть, что интересного найдётся в ваших собственных проектах :).


Даты


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


Встретились ошибки обработки дат и в Qt. Давайте с них и начнём.


Фрагмент N1: неправильная интерпретация статуса ошибки


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


static const char qt_shortMonthNames[][4] = {    "Jan", "Feb", "Mar", "Apr", "May", "Jun",    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};static int fromShortMonthName(QStringView monthName){  for (unsigned int i = 0;       i < sizeof(qt_shortMonthNames) / sizeof(qt_shortMonthNames[0]); ++i)  {    if (monthName == QLatin1String(qt_shortMonthNames[i], 3))      return i + 1;  }  return -1;}

В случае успеха функция возвращает номер месяца (значение от 1 до 12). Если имя месяца некорректно, то функция возвращает отрицательное значение (-1). Обратите внимание, что функция не может вернуть значение 0.


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


QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format){  ....  month = fromShortMonthName(parts.at(1));  if (month)    day = parts.at(2).toInt(&ok);  // If failed, try day then month  if (!ok || !month || !day) {    month = fromShortMonthName(parts.at(2));    if (month) {      QStringView dayPart = parts.at(1);      if (dayPart.endsWith(u'.'))        day = dayPart.chopped(1).toInt(&ok);    }  }  ....}

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


  • V547 [CWE-571] Expression 'month' is always true. qdatetime.cpp 4907
  • V560 [CWE-570] A part of conditional expression is always false: !month. qdatetime.cpp 4911
  • V547 [CWE-571] Expression 'month' is always true. qdatetime.cpp 4913
  • V560 [CWE-570] A part of conditional expression is always false: !month. qdatetime.cpp 4921

Фрагмент N2: ошибка логики обработки даты


Для начала посмотрим на реализацию функции, возвращающей количество секунд.


enum {  ....  MSECS_PER_DAY = 86400000,  ....  SECS_PER_MIN = 60,};int QTime::second() const{    if (!isValid())        return -1;    return (ds() / 1000)%SECS_PER_MIN;}

Рассмотренная функция может вернуть значение в диапазоне [0..59] или статус ошибки -1.


В одном месте эта функция используется очень странным образом:


static qint64 qt_mktime(QDate *date, QTime *time, ....){  ....  } else if (yy == 1969 && mm == 12 && dd == 31             && time->second() == MSECS_PER_DAY - 1) {      // There was, of course, a last second in 1969, at time_t(-1); we won't      // rescue it if it's not in normalised form, and we don't know its DST      // status (unless we did already), but let's not wantonly declare it      // invalid.  } else {  ....}

Предупреждение PVS-Studio: V560 [CWE-570] A part of conditional expression is always false: time->second() == MSECS_PER_DAY 1. qdatetime.cpp 2488


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


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


time->second() == MSECS_PER_DAY - 1

MSECS_PER_DAY 1 это 86399999. Функция second, как мы уже знаем, никак не может вернуть такое значение. Таким образом, здесь какая-то логическая ошибка и код заслуживает пристального внимания разработчиков.


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


Опечатки


Фрагмент N3: неожиданно, мы поговорим о HTML!


QString QPixelTool::aboutText() const{  const QList<QScreen *> screens = QGuiApplication::screens();  const QScreen *windowScreen = windowHandle()->screen();  QString result;  QTextStream str(&result);  str << "<html></head><body><h2>Qt Pixeltool</h2><p>Qt " << QT_VERSION_STR    << "</p><p>Copyright (C) 2017 The Qt Company Ltd.</p><h3>Screens</h3><ul>";  for (const QScreen *screen : screens)    str << "<li>" << (screen == windowScreen ? "* " : "  ")        << screen << "</li>";  str << "<ul></body></html>";  return result;}

Предупреждение PVS-Studio: V735 Possibly an incorrect HTML. The "</ body>" closing tag was encountered, while the "</ ul>" tag was expected. qpixeltool.cpp 707


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


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


str << "</ul></body></html>";

Фрагмент N4: повторная проверка в условии


class Node{  ....  bool isGroup() const { return m_nodeType == Group; }  ....};void DocBookGenerator::generateDocBookSynopsis(const Node *node){  ....  if (node->isGroup() || node->isGroup()      || node->isSharedCommentNode() || node->isModule()      || node->isJsModule() || node->isQmlModule() || node->isPageNode())    return;  ....}

Предупреждение PVS-Studio: V501 [CWE-570] There are identical sub-expressions to the left and to the right of the '||' operator: node->isGroup() || node->isGroup() docbookgenerator.cpp 2599


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


Фрагмент N5: создание лишней локальной переменной


void MainWindow::addToPhraseBook(){  ....  QString selectedPhraseBook;  if (phraseBookList.size() == 1) {    selectedPhraseBook = phraseBookList.at(0);    if (QMessageBox::information(this, tr("Add to phrase book"),          tr("Adding entry to phrasebook %1").arg(selectedPhraseBook),           QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok)                          != QMessageBox::Ok)      return;  } else {    bool okPressed = false;    QString selectedPhraseBook =       QInputDialog::getItem(this, tr("Add to phrase book"),                            tr("Select phrase book to add to"),                            phraseBookList, 0, false, &okPressed);    if (!okPressed)      return;  }  MessageItem *currentMessage = m_dataModel->messageItem(m_currentIndex);  Phrase *phrase = new Phrase(currentMessage->text(),                              currentMessage->translation(),                              QString(), nullptr);  phraseBookHash.value(selectedPhraseBook)->append(phrase);}

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


Единорог из старой коллекции


Предупреждение PVS-Studio: V561 [CWE-563] It's probably better to assign value to 'selectedPhraseBook' variable than to declare it anew. Previous declaration: mainwindow.cpp, line 1303. mainwindow.cpp 1313


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


QString selectedPhraseBook =

В результате в else-блоке появилась ещё одна локальная строковая переменная, которая инициализируется, но не используется. Тем временем переменная с таким же именем во внешнем блоке останется пустой.


Фрагмент N6: приоритет операций


Классический паттерн ошибки, который встречается весьма часто.


bool QQmlImportInstance::resolveType(....){  ....  if (int icID = containingType.lookupInlineComponentIdByName(typeStr) != -1)  {    *type_return = containingType.lookupInlineComponentById(icID);  } else {    auto icType = createICType();    ....  }  ....}

Предупреждение PVS-Studio: V593 [CWE-783] Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. qqmlimport.cpp 754


Значение переменной icID всегда будет иметь значение 0 или 1. Это явно не то, что задумывалось. Причина: в начале происходит сравнение с -1, а только затем инициализация переменной icID.


Современный синтаксис C++ позволяет корректно записать это условие следующим образом:


if (int icID = containingType.lookupInlineComponentIdByName(typeStr);    icID != -1)

Кстати, очень похожую ошибку мы уже обнаруживали в Qt:


char ch;while (i < dataLen && ((ch = data.at(i) != '\n') && ch != '\r'))  ++i;

Но пока на вооружение не будет взят такой анализатор кода, как PVS-Studio, программисты вновь и вновь будут допускать такие ошибки. Никто не совершенен. И да, это тонкий намёк внедрить PVS-Studio :).


Фрагмент N7: коварное деление по модулю


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


if (A % 2 == 1)

Но программисты вновь и вновь ошибаются и пишут что-то типа этого:


if (A % 1 == 1)

Это естественно неправильно, так как остаток от деления по модулю на один это всегда ноль. Не обошлось без этой ошибки и в Qt:


bool loadQM(Translator &translator, QIODevice &dev, ConversionData &cd){  ....  case Tag_Translation: {    int len = read32(m);    if (len % 1) {                                             // <=      cd.appendError(QLatin1String("QM-Format error"));      return false;    }    m += 4;    QString str = QString((const QChar *)m, len/2);  ....}

Предупреждение PVS-Studio: V1063 The modulo by 1 operation is meaningless. The result will always be zero. qm.cpp 549


Фрагмент N8: перезаписывание значения


QString Node::qualifyQmlName(){  QString qualifiedName = m_name;  if (m_name.startsWith(QLatin1String("QML:")))    qualifiedName = m_name.mid(4);  qualifiedName = logicalModuleName() + "::" + m_name;  return qualifiedName;}

Предупреждение PVS-Studio: V519 [CWE-563] The 'qualifiedName' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1227, 1228. node.cpp 1228


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


QString qualifiedName = m_name;if (m_name.startsWith(QLatin1String("QML:")))  qualifiedName = m_name.mid(4);qualifiedName = logicalModuleName() + "::" + qualifiedName;return qualifiedName;

Фрагмент N9: copy-paste


class Q_CORE_EXPORT QJsonObject{  ....  bool operator<(const iterator& other) const  { Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }  bool operator<=(const iterator& other) const  { Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }  ....}

Прудпреждение PVS-Studio: V524 It is odd that the body of '<=' function is fully equivalent to the body of '<' function. qjsonobject.h 155


Такие скучные функции, как операторы сравнения, никто не проверяет. На них обычно не пишут тесты. Их не просматривают или делают это очень быстро на code review. А зря. Статический анализ кода здесь как нельзя кстати. Анализатор не устаёт и с удовольствием проверяет даже такой скучный код.


Реализация операторов < и <= совпадают. Это явно неправильно. Скорее всего, код писался методом Copy-Paste, и затем забыли изменить всё что нужно в скопированном коде. Правильно:


bool operator<(const iterator& other) const{ Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }bool operator<=(const iterator& other) const{ Q_ASSERT(item.o == other.item.o); return item.index <= other.item.index; }

Фрагмент N10: static_cast / dynamic_cast


void QSGSoftwareRenderThread::syncAndRender(){  ....  bool canRender = wd->renderer != nullptr;  if (canRender) {     auto softwareRenderer = static_cast<QSGSoftwareRenderer*>(wd->renderer);     if (softwareRenderer)       softwareRenderer->setBackingStore(backingStore);  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'softwareRenderer' is always true. qsgsoftwarethreadedrenderloop.cpp 510


В начале рассмотрим вот эту проверку:


bool canRender = wd->renderer != nullptr;if (canRender) {

Благодаря ей можно быть уверенным, что внутри тела условного оператора значение указателя wd->renderer всегда точно ненулевое. Тогда непонятно, что же хотят проверить следующим кодом?


auto softwareRenderer = static_cast<QSGSoftwareRenderer*>(wd->renderer);if (softwareRenderer)

Если указатель wd->renderer ненулевой, то и указатель softwareRenderer точно ненулевой. Есть подозрение, что здесь опечатка, которая состоит в том, что на самом деле следовало использовать dynamic_cast. В этом случае код начинает приобретать смысл. Если преобразование типа невозможно, оператор dynamic_cast возвращает nullptr, и это возвращенное значение, естественно, следует проверять. Впрочем, возможно, я неправильно интерпретировал ситуацию и код нужно исправлять другим способом.


Фрагмент N11: скопировали блок кода и забыли изменить


void *QQuickPath::qt_metacast(const char *_clname){  if (!_clname) return nullptr;  if (!strcmp(_clname, qt_meta_stringdata_QQuickPath.stringdata0))    return static_cast<void*>(this);  if (!strcmp(_clname, "QQmlParserStatus"))    return static_cast< QQmlParserStatus*>(this);  if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))   // <=    return static_cast< QQmlParserStatus*>(this);  if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))   // <=    return static_cast< QQmlParserStatus*>(this);  return QObject::qt_metacast(_clname);}

Предупреждение PVS-Studio: V581 [CWE-670] The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 2719, 2721. moc_qquickpath_p.cpp 2721


Эти две строчки:


if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))  return static_cast< QQmlParserStatus*>(this);

Были размножены с помощью Copy-Paste. После чего они не были модифицированы и не имеют смысла.


Фрагмент N12: переполнение из-за не там поставленной скобки


int m_offsetFromUtc;....void QDateTime::setMSecsSinceEpoch(qint64 msecs){  ....  if (!add_overflow(msecs, qint64(d->m_offsetFromUtc * 1000), &msecs))    status |= QDateTimePrivate::ValidWhenMask;  ....}

Предупреждение PVS-Studio: V1028 [CWE-190] Possible overflow. Consider casting operands of the 'd->m_offsetFromUtc * 1000' operator to the 'qint64' type, not the result. qdatetime.cpp 3922


Программист предвидит ситуацию, что при умножении переменной типа int на 1000 может произойти переполнение. Чтобы этого избежать, он планирует использовать при умножении 64-битный тип qint64. И использует явное приведение типа.


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


add_overflow(msecs, qint64(d->m_offsetFromUtc) * 1000, &msecs)

Фрагмент N13: не полностью инициализированный массив


class QPathEdge{  ....private:  int m_next[2][2];  ....};inline QPathEdge::QPathEdge(int a, int b)    : flag(0)    , windingA(0)    , windingB(0)    , first(a)    , second(b)    , angle(0)    , invAngle(0){    m_next[0][0] = -1;    m_next[1][0] = -1;    m_next[0][0] = -1;    m_next[1][0] = -1;}

Предупреждения PVS-Studio:


  • V1048 [CWE-1164] The 'm_next[0][0]' variable was assigned the same value. qpathclipper_p.h 301
  • V1048 [CWE-1164] The 'm_next[1][0]' variable was assigned the same value. qpathclipper_p.h 302

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


m_next[0][0] = -1;m_next[0][1] = -1;m_next[1][0] = -1;m_next[1][1] = -1;

Я очень люблю такие примеры ошибок, которые встречаются в коде профессиональных разработчиков. Это как раз такой случай. Он показывает, что любой может опечататься, и поэтому статический анализ является вашим другом. Дело в том, что я уже десяток лет веду бой со скептиками, которые уверены, что такие ошибки можно встретить только в лабораторных работах студентов и что они такие ошибки никогда не делают :). Ещё 10 лет назад я написал заметку "Миф второй профессиональные разработчики не допускают глупых ошибок", и с тех пор, естественно, ничего не изменилось. Люди всё также делают такие ошибки и всё также утверждают, что это не так :).


Будь мудр - используй статический анализатор кода


Ошибки в логике


Фрагмент N14: недостижимый код


void QmlProfilerApplication::tryToConnect(){  Q_ASSERT(!m_connection->isConnected());  ++ m_connectionAttempts;  if (!m_verbose && !(m_connectionAttempts % 5)) {// print every 5 seconds    if (m_verbose) {      if (m_socketFile.isEmpty())        logError(          QString::fromLatin1("Could not connect to %1:%2 for %3 seconds ...")          .arg(m_hostName).arg(m_port).arg(m_connectionAttempts));      else        logError(          QString::fromLatin1("No connection received on %1 for %2 seconds ...")          .arg(m_socketFile).arg(m_connectionAttempts));    }  }  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'm_verbose' is always false. qmlprofilerapplication.cpp 495


Этот код никогда ничего не запишет в лог. Причиной являются противоположные условия:


if (!m_verbose && ....) {  if (m_verbose) {

Фрагмент N15: перетирание значения переменной


void QRollEffect::scroll(){  ....  if (currentHeight != totalHeight) {      currentHeight = totalHeight * (elapsed/duration)          + (2 * totalHeight * (elapsed%duration) + duration)          / (2 * duration);      // equiv. to int((totalHeight*elapsed) / duration + 0.5)      done = (currentHeight >= totalHeight);  }  done = (currentHeight >= totalHeight) &&         (currentWidth >= totalWidth);  ....}

Предупреждение PVS-Studio: V519 [CWE-563] The 'done' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 509, 511. qeffects.cpp 511


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


Фрагмент N16-N20: перетирание значения переменной


Альтернативный вариант перетирания значения переменной.


bool QXmlStreamWriterPrivate::finishStartElement(bool contents){  ....  if (inEmptyElement) {    ....    lastNamespaceDeclaration = tag.namespaceDeclarationsSize;   // <=    lastWasStartElement = false;  } else {    write(">");  }  inStartElement = inEmptyElement = false;  lastNamespaceDeclaration = namespaceDeclarations.size();      // <=  return hadSomethingWritten;}

Предупреждение PVS-Studio: V519 [CWE-563] The 'lastNamespaceDeclaration' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 3030, 3036. qxmlstream.cpp 3036


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


Есть ещё 4 предупреждения, указывающих на такой же паттерн ошибки:


  • V519 [CWE-563] The 'last' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 609, 637. qtextengine.cpp 637
  • V519 [CWE-563] The 'm_dirty' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1014, 1017. qquickshadereffect.cpp 1017
  • V519 [CWE-563] The 'changed' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 122, 128. qsgdefaultspritenode.cpp 128
  • V519 [CWE-563] The 'eaten' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 299, 301. qdesigner.cpp 301

Фрагмент N21: путаница между нулевым указателем и пустой строкой


// this could become a list of all languages used for each writing// system, instead of using the single most common language.static const char languageForWritingSystem[][6] = {    "",     // Any    "en",  // Latin    "el",  // Greek    "ru",  // Cyrillic    ...... // Нулевых указателей нет. Используются пустые строковые литералы.    "", // Symbol    "sga", // Ogham    "non", // Runic    "man" // N'Ko};static void populateFromPattern(....){  ....  for (int j = 1; j < QFontDatabase::WritingSystemsCount; ++j) {    const FcChar8 *lang = (const FcChar8*) languageForWritingSystem[j];    if (lang) {  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'lang' is always true. qfontconfigdatabase.cpp 462


В массиве languageForWritingSystem нет нулевых указателей. Поэтому проверка if(lang) не имеет смысла. Зато в массиве есть пустые строки. Быть может, хотелось сделать проверку именно на пустую строку? Если да, тогда корректный код должен выглядеть так:


if (strlen(lang) != 0) {

Или можно ещё проще написать:


if (lang[0] != '\0') {

Фрагмент N22: странная проверка


bool QNativeSocketEnginePrivate::createNewSocket(....){  ....  int socket = qt_safe_socket(domain, type, protocol, O_NONBLOCK);  ....  if (socket < 0) {    ....    return false;  }  socketDescriptor = socket;  if (socket != -1) {    this->socketProtocol = socketProtocol;    this->socketType = socketType;  }  return true;}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'socket != 1' is always true. qnativesocketengine_unix.cpp 315


Условие socket != -1 всегда истинно, так как выше происходит выход из функции, если значение переменной socket отрицательное.


Фрагмент N23: так что же всё-таки должна вернуть функция?


bool QSqlTableModel::removeRows(int row, int count, const QModelIndex &parent){  Q_D(QSqlTableModel);  if (parent.isValid() || row < 0 || count <= 0)    return false;  else if (row + count > rowCount())    return false;  else if (!count)    return true;  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression '!count' is always false. qsqltablemodel.cpp 1110


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


if (.... || count <= 0)  return false;....else if (!count)  return true;

Первая проверка говорит нам, что если значение count меньше или равно 0, то это ошибочное состояние и функция должна вернуть false. Однако ниже мы видим точное сравнение этой переменной с нулём, и этот случай уже интерпретируется по-другому: функция должна вернуть true.


Здесь явно что-то не так. Я подозреваю, что на самом деле проверка должна быть не <=, а просто <. Тогда код обретает смысл:


bool QSqlTableModel::removeRows(int row, int count, const QModelIndex &parent){  Q_D(QSqlTableModel);  if (parent.isValid() || row < 0 || count < 0)    return false;  else if (row + count > rowCount())    return false;  else if (!count)    return true;  ....}

Фрагмент N24: лишний статус?


В следующем коде переменная identifierWithEscapeChars выглядит просто как лишняя сущность. Или это логическая ошибка? Или код не дописан? К моменту второй проверки эта переменная в любом случае всегда будет равна true.


int Lexer::scanToken(){  ....  bool identifierWithEscapeChars = false;  ....  if (!identifierWithEscapeChars) {    identifierWithEscapeChars = true;    ....  }  ....  if (identifierWithEscapeChars) {    // <=    ....  }  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'identifierWithEscapeChars' is always true. qqmljslexer.cpp 817


Фрагмент N25: что делать с девятью объектами?


bool QFont::fromString(const QString &descrip){  ....  const int count = l.count();  if (!count || (count > 2 && count < 9) || count == 9 || count > 17 ||      l.first().isEmpty()) {    qWarning("QFont::fromString: Invalid description '%s'",             descrip.isEmpty() ? "(empty)" : descrip.toLatin1().data());    return false;  }  setFamily(l[0].toString());  if (count > 1 && l[1].toDouble() > 0.0)    setPointSizeF(l[1].toDouble());  if (count == 9) {                           // <=    setStyleHint((StyleHint) l[2].toInt());    setWeight(QFont::Weight(l[3].toInt()));    setItalic(l[4].toInt());    setUnderline(l[5].toInt());    setStrikeOut(l[6].toInt());    setFixedPitch(l[7].toInt());  } else if (count >= 10) {  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'count == 9' is always false. qfont.cpp 2142


Как должна себя вести функция, если переменная count равна 9? С одной стороны, функция должна выдать предупреждение и завершить свою работу. Ведь явно написано:


if (.... || count == 9 || ....) {  qWarning(....);  return false;}

С другой стороны, для 9 объектов предусмотрено выполнение специального кода:


if (count == 9) {  setStyleHint((StyleHint) l[2].toInt());  setWeight(QFont::Weight(l[3].toInt()));  setItalic(l[4].toInt());  ....}

Этот код, конечно, никогда не выполняется. Код ждёт, чтобы его пришли и исправили :).


Нулевые указатели


Фрагмент N26-N42: использование указателя до его проверки


class __attribute__((visibility("default"))) QMetaType {  ....  const QtPrivate::QMetaTypeInterface *d_ptr = nullptr;};QPartialOrdering QMetaType::compare(const void *lhs, const void *rhs) const{    if (!lhs || !rhs)        return QPartialOrdering::Unordered;    if (d_ptr->flags & QMetaType::IsPointer)        return threeWayCompare(*reinterpret_cast<const void * const *>(lhs),                               *reinterpret_cast<const void * const *>(rhs));    if (d_ptr && d_ptr->lessThan) {        if (d_ptr->equals && d_ptr->equals(d_ptr, lhs, rhs))            return QPartialOrdering::Equivalent;        if (d_ptr->lessThan(d_ptr, lhs, rhs))            return QPartialOrdering::Less;        if (d_ptr->lessThan(d_ptr, rhs, lhs))            return QPartialOrdering::Greater;        if (!d_ptr->equals)            return QPartialOrdering::Equivalent;    }    return QPartialOrdering::Unordered;}

Предупреждение PVS-Studio: V595 [CWE-476] The 'd_ptr' pointer was utilized before it was verified against nullptr. Check lines: 710, 713. qmetatype.cpp 710


Ошибка на первый взгляд может быть не заметна. Но на самом деле всё просто. Проследим, как работают с указателем d_ptr:


if (d_ptr->flags & ....)if (d_ptr && ....)

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


Это один из самых распространённых паттернов ошибки в языке C и С++. Пруфы. В исходных Qt кодах тоже встречается немало ошибок этой разновидности:


  • V595 [CWE-476] The 'self' pointer was utilized before it was verified against nullptr. Check lines: 1346, 1351. qcoreapplication.cpp 1346
  • V595 [CWE-476] The 'currentTimerInfo' pointer was utilized before it was verified against nullptr. Check lines: 636, 641. qtimerinfo_unix.cpp 636
  • V595 [CWE-476] The 'lib' pointer was utilized before it was verified against nullptr. Check lines: 325, 333. qlibrary.cpp 325
  • V595 [CWE-476] The 'fragment.d' pointer was utilized before it was verified against nullptr. Check lines: 2262, 2266. qtextcursor.cpp 2262
  • V595 [CWE-476] The 'window' pointer was utilized before it was verified against nullptr. Check lines: 1581, 1583. qapplication.cpp 1581
  • V595 [CWE-476] The 'window' pointer was utilized before it was verified against nullptr. Check lines: 1593, 1595. qapplication.cpp 1593
  • V595 [CWE-476] The 'newHandle' pointer was utilized before it was verified against nullptr. Check lines: 873, 879. qsplitter.cpp 873
  • V595 [CWE-476] The 'targetModel' pointer was utilized before it was verified against nullptr. Check lines: 454, 455. qqmllistmodel.cpp 454
  • V595 [CWE-476] The 'childIface' pointer was utilized before it was verified against nullptr. Check lines: 102, 104. qaccessiblequickitem.cpp 102
  • V595 [CWE-476] The 'e' pointer was utilized before it was verified against nullptr. Check lines: 94, 98. qquickwindowmodule.cpp 94
  • V595 [CWE-476] The 'm_texture' pointer was utilized before it was verified against nullptr. Check lines: 235, 239. qsgplaintexture.cpp 235
  • V595 [CWE-476] The 'm_unreferencedPixmaps' pointer was utilized before it was verified against nullptr. Check lines: 1140, 1148. qquickpixmapcache.cpp 1140
  • V595 [CWE-476] The 'camera' pointer was utilized before it was verified against nullptr. Check lines: 263, 264. assimpimporter.cpp 263
  • V595 [CWE-476] The 'light' pointer was utilized before it was verified against nullptr. Check lines: 273, 274. assimpimporter.cpp 273
  • V595 [CWE-476] The 'channel' pointer was utilized before it was verified against nullptr. Check lines: 337, 338. assimpimporter.cpp 337
  • V595 [CWE-476] The 'm_fwb' pointer was utilized before it was verified against nullptr. Check lines: 2492, 2500. designerpropertymanager.cpp 2492

Фрагмент N43: использование указателя до его проверки в рамках одного выражения


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


void QFormLayoutPrivate::updateSizes(){  ....  QFormLayoutItem *field = m_matrix(i, 1);  ....  if (userHSpacing < 0 && !wrapAllRows && (label || !field->fullRow) && field)  ....}

Предупреждение PVS-Studio: V713 [CWE-476] The pointer 'field' was utilized in the logical expression before it was verified against nullptr in the same logical expression. qformlayout.cpp 405


Минутка отдыха


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


И пока все ушли на кухню, рекламная пауза. Приглашаю команду, занимающуюся разработкой проекта Qt рассмотреть вопрос приобретения лицензии на анализатор кода PVS-Studio. Запросить прайс можно здесь. С нашей стороны поддержка и помощь в настройке. Да, согласен, я сегодня более навязчив, чем обычно. Это эксперимент :).


Минутка отдыха с единорогом


Фрагмент N44-N72: нет проверки, что вернула функция malloc


void assignData(const QQmlProfilerEvent &other){  if (m_dataType & External) {    uint length = m_dataLength * (other.m_dataType / 8);    m_data.external = malloc(length);                          // <=    memcpy(m_data.external, other.m_data.external, length);    // <=  } else {    memcpy(&m_data, &other.m_data, sizeof(m_data));  }}

Предупреждение PVS-Studio: V575 [CWE-628] The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 277, 276. qqmlprofilerevent_p.h 277


Нельзя просто взять и использовать указатель, который вернула функция malloc. Нужно обязательно проверить, не является ли этот указатель нулевым, даже если очень лень всем этим заниматься. Для этого есть 4 причины, которые описаны в статье "Почему важно проверять, что вернула функция malloc".


Перед нами один из случаев, где отсутствует необходимая проверка. Есть и другие предупреждения, но из-за их количества включать весь список в статью не хочется. На всякий случай, я выписал 28 предупреждений в файл: qt6-malloc.txt. Но на самом деле разработчикам, конечно, лучше самим перепроверить проект и самостоятельно изучить предупреждения. У меня не было задачи выявить как можно больше ошибок.


Что интересно, на фоне важных забытых проверок есть совершенно ненужные. Речь идёт о вызове оператора new, который в случае ошибки выделения памяти сгенерирует исключение std::bad_alloc. Вот один из примеров такой избыточной проверки:


static QImageScaleInfo* QImageScale::qimageCalcScaleInfo(....){  ....  QImageScaleInfo *isi;  ....  isi = new QImageScaleInfo;  if (!isi)    return nullptr;  ....}

Предупреждение PVS-Studio: V668 [CWE-570] There is no sense in testing the 'isi' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. qimagescale.cpp 245


P.S. Здесь читатели всегда задают вопрос, учитывает ли анализатор placement new или "new (std::nothrow) T"? Да, учитывает и не выдаёт для них ложные срабатывания.


Избыточный код ("код с запахом")


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


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


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


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


Фрагмент N73: "код с запахом" обратная проверка


void QQuick3DSceneManager::setWindow(QQuickWindow *window){  if (window == m_window)    return;  if (window != m_window) {    if (m_window)      disconnect(....);    m_window = window;    connect(....);    emit windowChanged();  }}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'window != m_window' is always true. qquick3dscenemanager.cpp 60


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


Фрагмент N74: "код с запахом" странная инициализация


QModelIndex QTreeView::moveCursor(....){  ....  int vi = -1;  if (vi < 0)    vi = qMax(0, d->viewIndex(current));  ....}

Предупреждение PVS-Stduio: V547 [CWE-571] Expression 'vi < 0' is always true. qtreeview.cpp 2219


Что это? Зачем так писать?


Что это? Зачем так писать? Код можно упростить до одной строки:


int vi = qMax(0, d->viewIndex(current));

Фрагмент N75: "код с запахом" недостижимый код


bool copyQtFiles(Options *options){  ....  if (unmetDependencies.isEmpty()) {    if (options->verbose) {      fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",              qPrintable(sourceFileName));    }  } else {    if (unmetDependencies.isEmpty()) {      if (options->verbose) {        fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",                  qPrintable(sourceFileName));      }    } else {      fprintf(stdout, "  -- Skipping %s. It has unmet dependencies: %s.\n",              qPrintable(sourceFileName),              qPrintable(unmetDependencies.join(QLatin1Char(','))));    }  }  ....}

Предупреждение PVS-Studio: V571 [CWE-571] Recurring check. The 'if (unmetDependencies.isEmpty())' condition was already verified in line 2203. main.cpp 2209


На первый взгляд перед нами респектабельный код, формирующий подсказку. Но давайте приглядимся. Если первый раз условие unmetDependencies.isEmpty() выполнилось, то второй раз этого уже не произойдёт. Это нестрашно, так как автор планировал вывести то же самое сообщение. Настоящей ошибки нет, но код переусложнён. Он может быть упрощен до следующего варианта:


bool copyQtFiles(Options *options){  ....  if (unmetDependencies.isEmpty()) {    if (options->verbose) {      fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",              qPrintable(sourceFileName));    }  } else {    fprintf(stdout, "  -- Skipping %s. It has unmet dependencies: %s.\n",            qPrintable(sourceFileName),            qPrintable(unmetDependencies.join(QLatin1Char(','))));  }  ....}

Фрагмент N76: "код с запахом" сложный тернарный оператор


bool QDockAreaLayoutInfo::insertGap(....){  ....  QDockAreaLayoutItem new_item    = widgetItem == nullptr      ? QDockAreaLayoutItem(subinfo)      : widgetItem ? QDockAreaLayoutItem(widgetItem)                    : QDockAreaLayoutItem(placeHolderItem);  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'widgetItem' is always true. qdockarealayout.cpp 1167


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


  QDockAreaLayoutItem new_item    = widgetItem == nullptr      ? QDockAreaLayoutItem(subinfo) : QDockAreaLayoutItem(widgetItem);

Фрагмент N77: "код с запахом" избыточная защита


typedef unsigned int uint;ReturnedValue TypedArrayCtor::virtualCallAsConstructor(....){  ....  qint64 l = argc ? argv[0].toIndex() : 0;  if (scope.engine->hasException)    return Encode::undefined();  // ### lift UINT_MAX restriction  if (l < 0 || l > UINT_MAX)    return scope.engine->throwRangeError(QLatin1String("Index out of range."));  uint len = (uint)l;  if (l != len)    scope.engine->throwRangeError(      QStringLiteral("Non integer length for typed array."));  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'l != len' is always false. qv4typedarray.cpp 306


Кто-то очень переживает, что значение 64-битной переменной не вмещается в 32-битную переменную unsigned. И использует сразу две проверки корректности. При этом вторая проверка избыточна.


Вот этого условия более чем достаточно:


if (l < 0 || l > UINT_MAX)

Приведенный ниже фрагмент можно смело удалить, и программа менее надёжной не станет:


uint len = (uint)l;if (l != len)  scope.engine->throwRangeError(    QStringLiteral("Non integer length for typed array."));

Дальше продолжать не буду. Думаю, идею вы поняли.


Здесь можно сделать маленький вывод: результатом использования анализатора PVS-Studio будет не только устранение ошибок, но и упрощение кода.


Другие ошибки


Я остановился после того, как описал 77 дефектов. Это красивое число и выписанного более чем достаточно, чтобы написать статью. Однако это не значит, что нет других ошибок, которые способен выявить PVS-Studio. При изучении лога я был весьма поверхностен и пропускал всё, где нужно было разбираться более пары минут, ошибка это или нет :).


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


Заключение


Статический анализ это круто! После внедрения PVS-Studio будет экономить время и нервы, выявляя множество ошибок сразу после написания кода. Намного лучше искать на code review с коллегами не опечатки, а высокоуровневые ошибки и обсуждать эффективность реализованного алгоритма. Тем более, как показывает практика, эти дурацкие опечатки всё равно отлично прячутся при просмотре кода глазами. Так что пусть их лучше ищет программа, а не человек.


Если у вас ещё остались вопросы или возражения, приглашаю познакомиться со статьёй "Причины внедрить в процесс разработки статический анализатор кода PVS-Studio". С вероятность 90 % вы найдете в ней ответ на ваши вопросы :). В оставшихся 10 % случаев напишите нам, пообщаемся :).


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Date Processing Attracts Bugs or 77 Defects in Qt 6.

Подробнее..

Маленькое удобство, способное прекратить вселенские споры

22.06.2020 00:17:16 | Автор: admin
Все те, кто пишет на Си-подобных языках, знакомы с двумя немного отличающимися стилями кода. Выглядят они вот так:

for (int i=0;i<10;i++) {    printf("Hello world!");    printf("Hello world again!");}


for (int i=0;i<10;i++){    printf("Hello world!");    printf("Hello world again!");}


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

Компромиссное решение могло бы выглядеть вот так:

image

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

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

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

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

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

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

Алгоритм замены скобок.



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

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

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

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

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

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

Теперь изменения помельче. Если что-то правится внутри блока, то опять делать ничего не нужно. Но если к блоку добавляется новая строка, то самым логичным было бы ожидать, что разработчик установит курсор в конец последней строки и нажмёт enter. После этого мы ждём ввода первого символа и удаляем нижнее подчёркивание из начала последней строки и вставляем его в начало следующей (пока ещё пустой). Далее разработчик пишет текст после подчёркивания и некоторого набора пробелов (или табуляции), обозначающего отступ блока.

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

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

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

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

И напоследок о символе overscore, который я здесь условно назвал верхним подчёркиванием. Его, к сожалению, нет на стандартной клавиатуре, а потому нет возможности вводить его напрямую без помощи редактора. Именно поэтому важна помощь IDE. Хотя теоретически можно вводить последовательность юникода (\u00af = ), которую некоторые редакторы автоматически преобразуют в символ overscore, но всё же делать так каждый раз, когда нам нужна скобка, было бы просто издевательством над разработчиками. Поэтому и нужны плагины, ну и изменения в спецификациях языков.

Всё, ждём срочных обновлений спецификаций языков и массу удобных плагинов для всех возможных IDE :)
Подробнее..
Категории: Javascript , C++ , C , Java , Код , Си , Стиль кода

Как я разработал мобильную игру на Android с использованием React.js и выложил её в Google Play Store

30.12.2020 18:06:06 | Автор: admin

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

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

Скриншот готовой игрыСкриншот готовой игры

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

Предыстория

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

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

Построение и прорисовка мира

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

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

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

Сперва отрисуем ячейки мира построчно. Пускай они будут размером 64x64 пикселя. Далее развернём наш контейнер таким образом, чтобы он выглядел изометрично:

.rotate {  transform: rotateX(60deg) rotateZ(45deg);  transform-origin: left top;}

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

const cellOffsets = {};export function getCellOffset(n) {  if (n === 0) {    return 0;  }  if (cellOffsets[n]) {    return cellOffsets[n];  }  const result = 64 * (Math.floor(n / 2));  cellOffsets[n] = result;  return result;}

Использование:

import { getCellOffset } from 'libs/civilizations/helpers';// ...const offset = getCellOffset(columnIndex);// ...style={{  transform: `translateX(${(64 * rowIndex) + (64 * columnIndex) - offset}px) translateY(${(64 * rowIndex) - offset}px)`,}}

Для увеличения производительности нам необходимо перерисовывать только те ячейки карты, которые сейчас являются видимыми на экране. Для этого был использован компонент FixedSizeGrid из модифицированной версии библиотеки react-window с учетом нашего поворота и расположений ячеек, код которого здесь приводить не буду. Из того, что не получилось - это сделать бесконечную прокрутку мира. После изучений исходного различных библиотек для бесконечного скролла / слайдеров и тп. подходящего решения найдено не было. Что ж, значит наш мир будет с границами по всем бокам.

Графика

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

Игра до поиска графических элементовИгра до поиска графических элементов

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

Спрайт вертолётаСпрайт вертолёта

Локализация

Игра поддерживает 4 языка и, если честно, мне непонятно, зачем в несложных приложениях разработчики подключают массивные библиотеки типа react-i18next. Давайте напишем похожее кастомное решение, которое уместится в чуть более чем 100 строк с учетом красивой разметки кода, а также будет поддерживать определение языка девайса пользователя, переключение языков в реальном времени и сохранение последнего выбора пользователя. Здесь используется redux, однако данный код можно адаптировать и под другие реактивные хранилища. Да, здесь нет некоторых фишек больших библиотек типа поддержки переменных в строках, однако в таком проекте нам это и не нужно. И да, эту библиотеку можно использовать как легковесную замену react-i18next (или подобным) в уже существующем проекте.

import React, { Component } from 'react';import PropTypes from 'prop-types';import { connect } from 'react-redux';import get from 'lodash/get';import set from 'lodash/set';import size from 'lodash/size';import { emptyObj, EN, LANG, PROPS, langs } from 'defaults';import { getLang } from 'reducers/global/selectors';import en from './en';export function getDetectedLang() {  if (!global.navigator) {    return EN;  }  let detected;  if (size(navigator.languages)) {    detected = navigator.languages[0];  } else {    detected = navigator.language;  }  if (detected) {    detected = detected.substring(0, 2);    if (langs.indexOf(detected) !== -1) {      return detected;    }  }  return EN;}const options = {  lang: global.localStorage ?    (localStorage.getItem(LANG) || getDetectedLang()) :    getDetectedLang(),};const { lang: currentLang } = options;const translations = {  en,};if (!translations[currentLang]) {  try {    translations[currentLang] = require(`./${currentLang}`).default;  } catch (err) {} // eslint-disable-line}export function setLang(lang = EN) {  if (langs.indexOf(lang) === -1) {    return;  }  if (global.localStorage) {    localStorage.setItem(LANG, lang);  }  set(options, [LANG], lang);  if (!translations[lang]) {    try {      translations[lang] = require(`./${lang}`).default;    } catch (err) {} // eslint-disable-line  }}const mapStateToProps = (state) => {  return {    lang: getLang(state),  };};export function t(path) {  const { lang = get(options, [LANG], EN) } = get(this, [PROPS], emptyObj);  if (!translations[lang]) {    try {      translations[lang] = require(`./${lang}`).default;    } catch (err) {} // eslint-disable-line  }  return get(translations[lang], path) || get(translations[EN], path, path);}function i18n(Comp) {  class I18N extends Component {    static propTypes = {      lang: PropTypes.string,    }    static defaultProps = {      lang: EN,    }    constructor(props) {      super(props);      this.t = t.bind(this);    }    componentWillUnmount() {      this.unmounted = true;    }    render() {      return (        <Comp          {...this.props}          t={this.t}        />      );    }  }  return connect(mapStateToProps)(I18N);}export default i18n;

Использование:

import i18n from 'libs/i18n';// ...static propTypes = {  t: PropTypes.func,}// ...const { t } = this.props;// ...{t(['path', 'to', 'key'])}// ...или тоже самое, но слегка медленнее{t('path.to.key')}// ...export default i18n(Comp);

Мультиплеер

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

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

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

import isFunction from 'lodash/isFunction';let lastTime = 0;const vendors = ['ms', 'moz', 'webkit', 'o'];for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {  window.requestAnimationFrame = window[`${vendors[x]}RequestAnimationFrame`];  window.cancelAnimationFrame = window[`${vendors[x]}CancelAnimationFrame`] || window[`${vendors[x]}CancelRequestAnimationFrame`];}if (!window.requestAnimationFrame) {  window.requestAnimationFrame = (callback) => {    const currTime = new Date().getTime();    const timeToCall = Math.max(0, 16 - (currTime - lastTime));    const id = window.setTimeout(() => { callback(currTime + timeToCall); },      timeToCall);    lastTime = currTime + timeToCall;    return id;  };}if (!window.cancelAnimationFrame) {  window.cancelAnimationFrame = (id) => {    clearTimeout(id);  };}let lastFrame = null;let raf = null;const callbacks = [];const loop = (now) => {  raf = requestAnimationFrame(loop);  const deltaT = now - lastFrame;  // do not render frame when deltaT is too high  if (deltaT < 160) {    let callbacksLength = callbacks.length;    while (callbacksLength-- > 0) {      callbacks[callbacksLength](now);    }  }  lastFrame = now;};export function registerRafCallback(callback) {  if (!isFunction(callback)) {    return;  }  const index = callbacks.indexOf(callback);  // remove already existing the same callback  if (index !== -1) {    callbacks.splice(index, 1);  }  callbacks.push(callback);  if (!raf) {    raf = requestAnimationFrame(loop);  }}export function unregisterRafCallback(callback) {  const index = callbacks.indexOf(callback);  if (index !== -1) {    callbacks.splice(index, 1);  }  if (callbacks.length === 0 && raf) {    cancelAnimationFrame(raf);    raf = null;  }}

Использование:

import { registerRafCallback, unregisterRafCallback } from 'client/libs/raf';// ...registerRafCallback(this.cooldown);// ...componentWillUnmount() {  unregisterRafCallback(this.cooldown);}

Стандартная имплементация Lobby из библиотеки движка мне не подходила, так как она открывала ещё одно новое websocket-подключение на каждый инстанс игры, но мне также нужно было передавать данные пользователя и таблицу лидеров по своему уже существующему websocket-подключению, потому, чтобы не плодить подключения, здесь снова было использовано собственное решение на основе библиотеки primus. На стороне клиента подключение хендлится сбилдженной библиотекой от примуса, которое также выложил на npm с именем primus-client. Вы можете сами сбилдить себе подобную клиентскую библиотеку для определенной версии примуса через функцию save на стороне сервера.

Видео геймплея многопользовательского режима можно наблюдать ниже:

Звук и музыка

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

import { SOUND_VOLUME } from 'defaults';const Sound = {  audio: null,  volume: localStorage.getItem(SOUND_VOLUME) || 0.8,  play(path) {    const audio = new Audio(path);    audio.volume = Sound.volume;    if (Sound.audio) {      Sound.audio.pause();    }    audio.play();    Sound.audio = audio;  },};export function getVolume() {  return Sound.volume;}export function setVolume(volume) {  Sound.volume = volume;  localStorage.setItem(SOUND_VOLUME, volume);}export default Sound;

Использование:

import Sound from 'client/libs/sound';// ...Sound.play('/mp3/win.mp3');
Окно настроек игрыОкно настроек игры

Сборка проекта

Сборка web-части осуществляется вебпаком. Однако тут нужно учитывать особенности путей к файлам, ведь в процессе разработке на локалхосте или на сервере в продакшене они являются относительными корня домена, а для приложения в Cordova наши файлы будут размещены по протоколу file:// и потому после сборки нам необходимо провести некоторые преобразования, а именно:

const replace = require('replace-in-file');const path = require('path');const options = {  files: [    path.resolve(__dirname, './app/*.css'),    path.resolve(__dirname, './app/*.js'),    path.resolve(__dirname, './app/index.html'),  ],  from: [/url\(\/img/g, /href="\//g, /src="\//g, /"\/mp3/g],  to: ['url(./img', 'href="./', 'src="./', '"./mp3'],};replace(options)  .then((results) => {    console.log('Replacement results:', results);  })  .catch((error) => {    console.error('Error occurred:', error);  });

Итоги

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

Из того, что было задумано, но не получилось:

  1. Более сложный геймплей

  2. Бесконечная прокрутка карты по горизонтали

  3. Продвинутый ИИ компьютера

  4. Поддержка мультиплеера на всех устройствах

Дальнейшие планы

Сейчас понятно, что подобные игры мало кому интересны, так что в прогрессе изучение Unity, и возможно через некоторое время появится ещё одна игра в жанре tactical rts.

Можно потыкать?

Можно. Для интересующихся - ссылка на приложение на Google Play Store.

P.S. Отдельное спасибо музыканту Anton Zvarych за предоставленную фоновую музыку.

Подробнее..

HowToCode Адаптация системного подхода к разработке для React и TypeScript

07.03.2021 00:21:34 | Автор: admin

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

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

Кардинальным образом ситуация изменилась после того, как я прошел курс HowToCode[ссылка удалена модератором, т.к. нарушает правила]. В курсе описан системный и, как всё гениальное, простой и красивый подход к разработке, который сводит воедино анализ, проектирование, документацию, тестирование и разработку кода. Весь курс построен на использовании функциональной парадигмы и языка Scheme (диалекта Lisp), тем не менее, рекомендации вполне применимы и для других языков, а для JavaScript и TypeScript, к которым я постарался их адаптировать, так и вообще подходят отлично.

Результаты мне очень понравились:

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

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

  • В-третьих, я практически избавился от отладки: того самого процесса, когда код уже написан, но ещё не работает или работает не так как надо, чем дико раздражает

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

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

Но давайте уже перейдём к сути и посмотрим, как этот подход устроен.

Общая идея

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

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

Этапы проектирования

Подход включает 3 этапа:

  1. Анализ задачи и предметной области

  2. Проектирование структур данных

  3. Проектирование функций

Анализ задачи и предметной области

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

Входными данными для этого этапа является постановка задачи. Она может выглядеть как угодно: устная формулировка, пара предложений в тикете Jira или полноценное ТЗ на десятки страниц. На самом деле это не так уж и важно.

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

Процедура анализа задачи

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

Алгоритм

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

Шаг 1. Представить задачу в динамике

Шаг 2. Выделить константы, переменные и события.

Шаг 1. Представить задачу в динамике

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

  • в исходном состоянии

  • в различных промежуточных состояниях: возможно, появляется загрузка, или раскрываются какие-то списки или открываются какие-то окна

  • при окончании работы

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

Пример

Допустим, первоначальная постановка задачи звучала как: "Реализовать для web-портала раскрывающийся список, который бы показывал перечень сотрудников, входящих в учебную группу. Для каждого сотрудника должно быть указано ФИО и дата его рождения."

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

Начальное состояни

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

Промежуточные состояния

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

Когда список открывается, мы должны показать список сотрудников, входящих в группу, и изменить состояние индикатора. Скорее всего, выглядеть наше приложение станет как-то так:

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

Шаг 2. Выделить константы, переменные и события

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

Константы

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

  • оформление: цвета, шрифты, расстояния, логотипы и иконки, размеры сетки и экрана и т.д.

  • сетевые данные, например, адреса серверов,

  • строковые константы: названия, заголовки и т.п.

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

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

  • Адрес сервера, с которым работает наше приложение

  • Название приложения, которое отображается в виде заголовка

  • Оформление

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

Переменные

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

  • координаты объектов

  • значения таймера

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

Пример выделения переменных

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

У меня получился следующий список:

  • Состояние загрузки приложения: загружается или данные уже загружены

  • Группа, для описания которой нам, скорее всего, понадобится её идентификатор, название, статус открытия списка группы

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

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

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

События

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

Событиями могут быть:

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

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

  • Таймер

  • Получение данных с сервера и т.д.

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

  • загрузка данных приложения и

  • открытие и закрытие списка сотрудников по клику мыши на шапку группы

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

Теперь мы уже существенно продвинулись вперед:

  1. Во-первых, мы уже покрутили задачу в голове и более или менее её поняли

  2. Во-вторых, мы сделали её куда как более конкретной, свели все возможные варианты реализации списков к одному

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

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

Проектирование структуры данных

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

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

Назначение

Для чего нам это надо?

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

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

  3. И в-третьих, если структуры данных описать полностью и с примерами, как рекомендует алгоритм, то примеры становятся очень неплохим подспорьем для unit-тестов.

Алгоритм

Структуры мы описываем в следующем порядке:

  1. Фиксируем название структуры

  2. Описываем её тип

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

  4. Придумываем один - два примера заполненной структуры.

На этом этапе уже начинают играть роль используемые технологии. В нашем случае появляются TypeScript и JSDoc, но для других языков и платформ вполне может использоваться и что-то другое. Ну а раз мы говорим о веб-разработке и библиотеке React JS, то надо сразу отметить, что мы дальше будем использоваться понятия свойств (props) и состояний (state), характерных для React. Ничего страшного, если вы с этими понятиями не знакомы, думаю, что всё будет и так понятно.

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

Пример

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

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

Фиксируем название структуры. Название должно отражать суть структуры. В нашем случае, назовём её AppState - состояние приложения:

export interface AppState {}

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

Описание

Название переменной

Источник

Тип данных

Обязательность

Комментарий

Название приложения

title

Константы

Строка

+

Адрес сервера

backendAddress

Константы

Строка

+

Флаг загрузки данных

isLoading

Переменные

Логическое значение

+

true - данные загружаются

false - данные загружены

Данные об отображаемой группе

group

Переменные

Объект типа Group

-

На момент загрузки данные о группе не определены

Метод загрузки данных

loadData

События

Функция

+

Фиксируем это в коде:

export interface AppState {        title: string;        backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}

Пишем интерпретацию структуры

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

/** * Общее состояние приложения * @prop title - заголовок приложения * @prop backendAddress - адрес сервера * @prop isLoading - флаг загрузки данных (true - загружаются, false - загружены) * @prop group - данные об отображаемой группе. На момент загрузки данных group не определена * @method loadData - метод загрузки данных */export interface AppState {    title: string;    backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}

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

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

/** * Общее состояние приложения * @prop title - заголовок приложения * @prop backendAddress - адрес сервера * @prop isLoading - флаг загрузки данных (true - загружаются, false - загружены) * @prop group - данные об отображаемой группе. На момент загрузки данных group не определена * @method loadData - метод загрузки данных */export interface AppState {    title: string;    backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}/** * Данные об отображаемой группе*///TODOexport interface Group {}

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

Делать это мне всегда тяжело, потому что кажется, что примеры - это пустая трата времени. И каждый раз, когда я дохожу до написания unit-тестов, я радуюсь тому, что я всё-таки их написал. В примерах вместо методов я указываю заглушки и вместо содержания вложенных структур, в нашем случае group - ссылки на переменные нужного типа, для которых временно тоже делаю заглушки. Получается что-то вроде:

/** * Общее состояние приложения * @prop title - заголовок приложения * @prop backendAddress - адрес сервера * @prop isLoading - флаг загрузки данных (true - загружаются, false - загружены) * @prop group - данные об отображаемой группе. На момент загрузки данных group не определена * @method loadData - метод загрузки данных */export interface AppState {    title: string;    backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}//Пример 1const appState1: AppState = {    title: "Заголовок 1",    backendAddress: "/view_doc.html",    isLoading: true,    group: undefined,    loadData: () => {}}//Пример 2const appState2: AppState = {    title: "Заголовок 2",    backendAddress: "/view_doc_2.html",    isLoading: false,    group: group1, //Заглушка для вложенной структуры    loadData: () => {}}/** * Данные об отображаемой группе*///TODOexport interface Group {}//TODOconst group1 = {}

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

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

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

export default abstract class AbstractService {           /**     * Метод загрузки данных о группе с сервера     * @fires get_group_data - имя действия, вызываемого на сервере     * @returns данные о группе     */    abstract getGroupData(): Promise<Group>;}

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

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

Проектирование функций

Только теперь мы, наконец, переходим к разработке.

Как и для других этапов, для проектирования функций у нас есть свой рецепт - алгоритм:

Алгоритм первоначального проектирования функций

  1. Создаем заглушку. Заглушка - это определение функции, которое:

    • отражает правильное название функции

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

    • возвращает произвольный результат, но корректного типа

  2. Описываем входные, выходные данные и назначение функции

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

  4. Пишем тело функции.

  5. Если в процессе написания функции необходимо обращаться к другой функции, то мы:

    1. сразу описываем функцию и добавляем заглушку

    2. добавляем отметку (TODO), помечающую нашу функцию как объект списка задач

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

Пример проектирования функции

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

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

Шаг 1 - Создаем заглушку

Заглушка функции - это минимальное работоспособное её описание, в котором есть:

  • правильное название функции,

  • правильное количество и тип параметров и

  • возвращаемое значение, произвольное по содержанию, но правильного типа.

Для простой функции на TypeScript будет вполне достаточно написать что-то вроде:

export const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    return "6ч 18мин";}

где:

  • getWorkDuration - правильное название фукнции, то есть то, которое мы и дальше будем использовать

  • worktimeFrom: string, worktimeTo: string - два строковых параметра. Именно столько параметров и такого типа функция должна принимать

  • : string после закрывающей круглой скобки - тип возвращаемого значения

  • return "6ч 18мин" - возврат произвольного значения, но правильного типа из функции

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

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

Для функциональных компонентов:

const componentName = (props: PropsType) => { return <h1>componentName</h1> }

Для компонентов классов:

class componentName extends React.Component<PropsType, StateType>{    state = {        //произвольные значения для всех обязательных свойств состояния    }    render() {                return <h1>componentName</h1>     }}

где:

  • PropsType - это описание типа передаваемых свойств

  • StateType - описание типа состояния компонента

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

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

interface AppProps {}export default class App extends Component<AppProps, AppState> {    state = {        title: "Отображение списка групп",        backendAddress: "",        isLoading: true,        loadData: this.loadData.bind(this)    }    /**     * Метод загрузки данных с сервера     */    //TODO    loadData() {    }    render() {        return <h1>App</h1>    }}

Надо отметить пару особенностей этой реализации:

  1. В компонент App не передаётся никаких свойств "сверху", поэтому интерфейс AppProps пустой

  2. Тип состояния компонента AppState мы импортируем напрямую из описания типов, которое мы создали, когда проектировали структуры

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

Шаг 2 - Описываем входные и выходные данные

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

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

/** * Рассчитывает количество часов и минут между указанным начальным и конечным временем * Если время начала работы больше времени конца, то считается, что конечное время - это время следующих суток * @param worktimeFrom - время начала работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @param worktimeTo - время конца работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @return количество часов и минут между переданным временем в формате Хч Y?мин, например 6ч 18мин или 5ч, если количество часов полное *///TODOexport const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    return "6ч 18мин";}

И обязательно отмечаем нашу функцию маркером TODO, чтобы сразу бросалось в глаза, что функция не закончена.

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

Шаг 3. Тестирование

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

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

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

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

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

Зависимость содержания функций и тестов от типов данных в функциональном программировании

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

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

type TrafficLights = "красный" | "желтый" | "зеленый";

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

function trafficLightsFunction (trafficLights: TrafficLights) {    switch (trafficLights) {        case "красный":            ...        case "желтый":            ...        case "зеленый":            ...    }}

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

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

Получается, что тип входных данных определяет и внутреннюю структуру функции - шаблон, и сценарии тестирования.

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

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

Тип данных

Описание

Пример данных

Правила тестирования

1

Атомарные данные

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

Строка

Логическое значение

Число

Для строк допустим 1 сценарий тестирования

для логических значений - 2 (true / false)

Для чисел - 1 сценарий, но может потребоваться дополнительный сценарий для значения 0

2

Перечисления

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

Цвета светофора

Пятибальная шкала оценок

Размеры одежды

и т.д.

Для тестирования нужно, как минимум, столько сценариев, сколько классов значений в перечислении.

Поэтому, если вам нужно протестировать 100-бальную шкалу оценок, но при этом вы понимаете, что значения группируются в 4 класса:

1 - 25,

26 - 50,

51 - 75,

76 - 100

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

3

Интервалы

Числа в определённом диапазоне

Скорость автомобиля (0 - 300]

Температура воздуха

и т.д.

Необходимо протестировать:

значение внутри диапазона

граничные значения

4

Детализация

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

Подразделение расформировано или нет? Если расформировано, то какова дата расформирования

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

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

Должно быть, как минимум, столько тест-кейсов, сколько комбинаций подклассов может быть.

Для примера с подразделением должно быть минимум 2 сценария:

подразделение нерасформировано и

подразделение расформировано и указана дата расформирования

А для примера с типами вопросов - как минимум, по одному для каждого типа.

5

Сложный тип (объект)

Данные, состоящие из нескольких независимых частей

Объект "Сотрудник" имеющий поля:

id

ФИО

Дата рождения

Пол

и т.д.

Как минимум 2 сценария. При этом надо изменять значения всех полей

6

Массив

Набор данных, объем которых может изменяться

Список сотрудников

Массив учебных курсов, назначенных сотруднику

Как правило, несколько сценариев:

один, проверяющий работу функции при пустом массиве

один, проверяющий работу функции при массиве с одним элементом

один или несколько, проверяющих работу функции, когда элементов в массиве 2 и больше

Примеры тестов

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

/** * Рассчитывает количество часов и минут между указанным начальным и конечным временем * Если время начала работы больше времени конца, то считается, что конечное время - это время следующих суток * @param worktimeFrom - время начала работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @param worktimeTo - время конца работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @return количество часов и минут между переданным временем в формате Хч Y?мин, например 6ч 18мин или 5ч, если количество часов полное *///TODOexport const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    return "6ч 18мин";}

Итак, функция расчёта рабочего времени. Пусть вас не смущает тип string у параметров, каждый из параметров - это время, представленное в виде строки ЧЧ:ММ, то есть не атомарный тип, а интервал от 00:00 до 23:59. Согласно таблице, для интервалов надо проверить граничные значения и значения внутри диапазона. А так как параметра два, то это справедливо для каждого из них. Для этого нам потребуется 3 тест-кейса:

  1. Первый параметр выходит за граничное значение, а второй нет

  2. Второй параметр выходит за граничное значение, а первый - нет

  3. Оба параметра - в пределах нормальных значений

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

Название

worktimeFrom

worktimeTo

Ожидаемое значение

1

Проверка корректности worktimeFrom

Граничное значение, не входящее в диапазон, например

"24:00"

Нормальное значение, например

"18:00"

Исключение

2

Проверка корректности worktimeFrom

Нормальное значение, например

"18:00"

Граничное значение, не входящее в диапазон, например

"24:00"

Исключение

3

Нормальная работа,

worktimeFrom < worktimeTo

Нормальное значение, меньшее чем worktimeTo, например

"00:00"

Нормальное значение, большее чем worktimeFrom, например,

"23:59"

23ч 59мин

4

Нормальная работа,

worktimeFrom > worktimeTo

Нормальное значение, большее чем worktimeTo, например

"18:49"

Нормальное значение, меньшее чем worktimeFrom, например,

"10:49"

16ч

5

Нормальная работа worktimeFrom = worktimeTo

Нормальное значение, например,

"01:32"

Нормальное значение, например,

"01:32"

Тест-кейсы мы к тому же составили так, чтобы одновременно проверялись выводимые значения: с минутами и без. Остается только написать сами тесты. Я их пишу при помощи Jest и Enzyme - стандартной комбинации для React JS. В самом написании тестов особых премудростей нет, поэтому приведу только один пример:

describe('Тестирование расчёта времени между началом и концом работы', () => {        it('Когда начало и конец смены совпадают, функция возвращает 0ч', ()=>{        const result = getWorkDuration("01:32", "01:32");        expect(result).toBe("0ч");    });        //Подобным образом описываем все сценарии    ...});

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

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

interface AppProps {}export default class App extends Component<AppProps, AppState> {    state = {        title: "Отображение списка групп",        backendAddress: "",        isLoading: true,        loadData: this.loadData.bind(this)    }    /**     * Метод загрузки данных с сервера     */    //TODO    loadData() {    }    render() {        return <h1>App</h1>    }}

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

/** * Общее состояние приложения * @prop title - заголовок приложения * @prop backendAddress - адрес сервера * @prop isLoading - флаг загрузки данных (true - загружаются, false - загружены) * @prop group - данные об отображаемой группе. На момент загрузки данных group не определена * @method loadData - метод загрузки данных */export interface AppState {    title: string;    backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}

Для начала определимся, к какому типу данных относится эта структура:

  • Во-первых, это сложная структура, состоящая из нескольких полей и членов.

  • Во-вторых, среди полей есть зависимые. Так, наличие поля group зависит от состояния загрузки, пока данные не загружены, данных о группе - нет. Если есть зависимые данные, то можно смело относить эти данные к типу "Детализация".

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

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

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

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

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

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

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

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

Начнём с простого - с вызова метода loadData при загрузке компонента:

import React from 'react';import Enzyme, { mount, shallow } from 'enzyme';import Adapter from 'enzyme-adapter-react-16';Enzyme.configure({ adapter: new Adapter() });import App from './App';describe('App', () => {    test('Когда компонент App смонтирован, вызывается функция loadData', () => {        //Добавляем слушателя к функции loadData        const loadData = jest.spyOn(App.prototype, 'loadData');                //Монтируем компонент        const wrapper = mount(<App></App>);                //Проверяем количество вызовов функции loadData        expect(loadData.mock.calls.length).toBe(1);    });}

Здесь мы подключили enzyme к нашему файлу с тестами, импортировали сам компонент и написали первый тест. Внутри теста мы:

  1. добавили слушателя к функции loadData внутри компонента,

  2. смонтировали компонент в тестовой среде (то есть сымитировали его появление в приложении) и

  3. проверили, что компонент в ходе монтирования вызвал нашу функцию загрузки данных

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

test('Когда данные загружаются, отображаются только заголовок и спиннер', () => {        //Монтируем компонент        const wrapper = mount(<App></App>);        //Подменяем состояние компонента на нужное для тестирования        wrapper.setState({            title: "Заголовок 1",            backendAddress: "/view_doc.html",            isLoading: true,            group: undefined,            loadData: () => {}        })        //Проверяем правильность отображения компонента        //Проверяем наличие и содержание заголовка        expect(wrapper.find('h1').length).toBe(1);        expect(wrapper.find('h1').text()).toBe("Заголовок 1");        //Заглушка, отображающаяся в процессе загрузки        expect(wrapper.find(Spinner).length).toBe(1);        //Компонент, отображающий группу отображаться не должен        expect(wrapper.find(Group).length).toBe(0);    });

Этот сценарий мы реализуем по следующей схеме:

  1. Монтируем компонент

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

  3. Проверяем правильность отображения компонента

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

  • заглушка для отображения в процессе загрузки - спиннер и

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

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

Реализуем третий сценарий по аналогичной схеме:

test('Когда данные загружены, отображается заголовок и группа. Спиннер не отображается', () => {        const wrapper = mount(<App></App>);        wrapper.setState({            title: "Заголовок 2",            backendAddress: "/view_doc_2.html",            isLoading: false,            group: {                id: "1",                name: "Группа 1",                listOfCollaborators: []            },            loadData: () => {}        })        expect(wrapper.find('h1').length).toBe(1);        expect(wrapper.find('h1').text()).toBe("Заголовок 2");        expect(wrapper.find(Spinner).length).toBe(0);        expect(wrapper.find(Group).length).toBe(1);    });

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

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

Шаги 4 и 5. Реализация функций и формирование списка задач

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

  • во-первых, старайтесь писать более высокоуровневый код,

  • во-вторых, избегайте коварной ошибки, которая называется Knowledge Shift (сдвиг области знаний).

Высокоуровневый код

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

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

/** * Рассчитывает количество часов и минут между указанным начальным и конечным временем * Если время начала работы больше времени конца, то считается, что конечное время - это время следующих суток * @param worktimeFrom - время начала работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @param worktimeTo - время конца работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @return количество часов и минут между переданным временем в формате Хч Y?мин, например 6ч 18мин или 5ч, если количество часов полное *///TODOexport const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    return "6ч 18мин";}

Очевидно, что для того, чтобы получить разницу во времени между двумя параметрами, каждый из которых представляет собой строку в формате "ЧЧ:ММ", и вернуть эту разницу тоже в строковом формате, нам надо выполнить несколько действий:

  1. Привести параметры к числовым значениям, например, минутам, прошедшим с 00:00

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

  3. Привести получившуюся разницу к формату Xч Y?мин, который и вернуть из функции

И тут мы могли бы пойти двумя путями:

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

  • выделить каждый шаг в отдельную функцию.

В результате мы бы получили такую картину:

/** * Рассчитывает количество часов и минут между указанным начальным и конечным временем * Если время начала работы больше времени конца, то считается, что конечное время - это время следующих суток * @param worktimeFrom - время начала работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @param worktimeTo - время конца работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @return количество часов и минут между переданным временем в формате Хч Y?мин, например 6ч 18мин или 5ч, если количество часов полное */export const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    const worktimeFromInMinutes = getWorktimeToMinutes(worktimeFrom);    const worktimeToInMinutes = getWorktimeToMinutes(worktimeTo);    const minutesDiff = calcDiffBetweenWorktime(worktimeFromInMinutes, worktimeToInMinutes);    return convertDiffToString(minutesDiff);}/** * Вычиcляет количество минут, прошедших с начала суток * @param worktimeFrom - время в формате ЧЧ:ММ (от 00:00 до 23:59) * @returns количество минут, прошедших с 00ч 00минут *///TODOexport const getWorktimeToMinutes = (worktime: string): number => {    return 0;}/** * Вычисляет количество минут между началом и концом рабочего дня с учётом суток * @param worktimeFrom - время начала рабочего дня в виде количества минут, прошедших с начала суток * @param worktimeTo - время конца рабочего дня в виде количества минут, прошедших с начала суток * @returns количество минут между началом и концом рабочего дня с учётом суток *///TODOexport const calcDiffBetweenWorktime = (worktimeFrom: number, worktimeTo: number): number => {    return 0;}/** * Преобразовывает количество минут во время в формате Хч Y?мин * @param minutes - количество минут * @returns время в формате Хч Y?мин, например 6ч 18мин или 5ч *///TODOexport const convertDiffToString = (minutes: number): string => {    return "6ч 18мин";}

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

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

Knowledge Shift

Говоря о проектировании функций, конечно же, нельзя обойти стороной такую ошибку как Knowledge Shift.

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

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

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

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

export default class App extends Component<AppProps, AppState> {    state = {        title: "Отображение списка групп",        backendAddress: "",        isLoading: true,        group: undefined,        loadData: this.loadData.bind(this)    }    /**     * Метод загрузки данных с сервера     */    //TODO    loadData() {}    componentDidMount() {        //Вызываем метод loadData при загрузке приложения        this.loadData();    }    render() {        const {isLoading, group, title} = this.state;        return (            <div className="container">                <h1>{title}</h1>                {                    isLoading ?                    <Spinner/>                    //Структуру типа Group передаем на обработку отдельному компоненту                    : <Group group={group}></Group>                }            </div>        );    }}

Мы добавили в компонент реализацию метода жизненного цикла componentDidMount, который вызовет наш метод загрузки данных при отображении компонента. Но основной функционал у нас содержится в методе render. В нем мы выводим заголовок и, в зависимости от статуса загрузки, рисуем либо заглушку - спиннер, либо компонент Group.

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

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

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

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

Подробнее..

Реализация процессорной архитектуры из книги Чарльза Петцольда Код. Тайный язык информатики

06.02.2021 16:23:57 | Автор: admin

О книге

Наверное многие, из тех, кто увлекается изучением того, как работает компьютер на самом низком уровне читали такие книги как: Таненбаум "Архитектура компьютера" или Харрис, Харрис "Цифровая схемотехника и архитектура компьютера", которые безусловного являются объемлющими трудами и хорошими книгами для обучения. Но если вы не являетесь инженером, но всё равно хотите погрузиться в мир цифровых вычислений и более глубоко понять то, а как же работает компьютер. В этом вам сможет помочь книга Чальза Петцольда "Код. Тайный язык информатики", которая начинает свой рассказ от причин, по которым людям понадобился обмен информацией и её обработка , с какими проблемами при этом столкнулись и заканчивает рассказам об устройстве реальных операционных систем и процессоров и их архитектуру. При этом автором был отлично подобран уровень абстракции, которого он придерживался при написании. В каких-то моментах подробно описывая работу элементарных частей процессора, а в других рассказывая простыми словами о сложных вещах.

Описание архитектуры

Так в главе 17 "Автоматизация" автор, начиная с описания с того, как автоматизировать суммирование данных с промежуточным сохранением состояния между операциями, переходит к реализации архитектуры с набором команд, достаточным для исполнения практически любых вычислений. Эта архитектура отличается от используемых тем, что размерности шины адреса, шины данных, и машинного слова имеют разные размерности. Обращение к памяти является является побайтовой, что является достаточно обычным. Машинное слово фиксированной длины little-endian состоит из 12 бит или 3 байта, разделённых на две части. Младший байт слова содержит код операции содержит номер команды, старшие два - её аргумент. Из-за ограничений тракта данных на один такт процессора тратится 4 такта счётчика, из которых 3 уходят на чтение команды и 1 на исполнение вычислений. Архитектурой описывается один программно доступный регистр общего назначения, в котором сохраняется результаты выполнения операций с АЛУ, а также 2 регистра-флага, являющихся аналогами C и Z регистра CPSR архитектуры ARM и означающими то, имел ли результат последней операции, выполненной с помощью АЛУ, бит переноса или равнялся нулю соответственно. Архитектура описывает 12 команд, которые можно логически разделить на 4 группы:

  • Операции взаимодействия с внутренним регистром

  • Загрузить

  • Сохранить

  • Арифметические операции

  • Сложить

  • Вычесть

  • Сложить с переносом

  • Вычесть с переносом

  • Операции перехода (условного и безусловного)

  • Перейти

  • Перейти если 0

  • Перейти если перенос

  • Перейти если не 0

  • Перейти если не перенос

  • Операция останова

  • Остановить

Так как количество операций, которые могут быть закодированными с помощью 8 бит равняется 2^8=256, то для расширения архитектура оставляет 256-12=244 вариантов команд, к которым могут быть добавлены, например различные часто используемые арифметические операции

  • Побитовое И (AND)

  • Побитовое ИЛИ (OR)

  • Побитовое НЕ (NOT)

  • Побитовое исключающее ИЛИ (XOR)

  • Логический сдвиг влево

  • Логический сдвиг вправо

  • Арифметический сдвиг

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

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

Наверное самой важной частью системы является интерфейс взаимодействия с человеком. Так в главе 22 "Операционная система" Петцольд демонстрирует панель прямого доступа к памяти. Доступ к памяти осуществляется с помощью перехвата тракта адреса и остановки выполнения текущей программы. Адресация предполагается с помощью ручного ввода значения переключением тумблеров в состояние логического 0 или 1, содержимое ячейки сразу отображается с помощью световых индикаторов. Изменение значения ячейки осуществляется с помощью тумблеров данных и возможно только если активирован тумблер, разрешающий запись. Активация тумблер сброс отвечает за обнуление счётчика команд.

Реализация

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

Микроархитектура

Схема микроархитектуры, предлагаемая автором.

Для реализации микроархитектуры использовалась САПР для ПЛИС Quartus II 13.0sp1, предоставляющая широкие возможности для разработки и отладки ПЛИС. Несмотря на возможность описания архитектуры с помощью специализированных языков таких как VHDL и Verilog, для большей наглядности всё проектирование осуществлялось исключительно в графическом режиме. Широко использовались возможности встроенных функций таких как:

  • lpm_mux (сокр. от multiplexer)

  • lpm_decode

  • lpm_counter

  • lpm_ff (сокр. от flip-flop)

  • lpmaddsub

  • lpm_constant

На начальных этапах разработки тестирование проводилось в программе ModelSim, поставляемой в пакете программ для разработки. На финальных этапах отладка и тестирование производилось непосредственно на самой исполняющей плате с помощью встроенной утилиты In-System Memory Content Editor для изменения состояния входящих данных.

В качестве исполняющей платы была выбрана Cyclone II EP2C5 Mini Dev Board на основе чипа ПЛИС EP2C5T144C8, обладающий встроенным кварцевым генератором на 50 МГц, более 80 контактами интерфейса ввода/вывода общего назначения и разъёмом JTAG для прошивки и отладки. Техническим ограничением стал существенно меньший объем оперативной памяти, доступный для использования, вследствие чего для адресации используются только 13 младших бит в отличие от 16 бит, описанных в архитектуре. Однако это не вносит никаких критических изменений в работу процессора и может быть исправлено заменой исполняющей платы на более производительный аналог.

Внешний вид

Это изображение сформировало в голове довольно запоминающийся образ и с первого взгляда реализовать такой пульт достаточно не сложно. Для его реализации потребуется 16 + 8 + 3 = 27 двухпозиционных тумблеров, 8 светодиодов и непосредственно листовой материал на котором будет всё размещаться.

Использовались микротумблеры MTS-102 ON-ON. В нижнем положении соединяющие верхнюю ножку со средней, в верхнем - нижнюю со средней. Это было удобно, так как по всем нижним можно было пустить высокое напряжении, верхние подключить к земле и снимать значение со средних.

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

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

Большой проблемой стало нанесение надписей, для этого в Autodesk Fusion 360 была разработана схема расположения надписей на фронтальной панели и с помощью плоттера вырезан шаблон для покраски.

Для питание используется разъем GX16 5P, но для удобства использования впаянный в шнур с USB для удобства использования с обычными блоками питания.

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

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

Внутреннее пространство

Как уже было сказано выше в качестве вычислительного модуля была выбрана плата Cyclone II EP2C5 Mini Dev Board на основе чипа ПЛИС EP2C5T144C8. Для подключения тумблеров и светодиодов к плате было решено не использовать пайку и использовать провода, используемые для макетного моделирования. Однако другая сторона распаяна к тумблерам или светодиодам, к которым относятся.

Так как хотелось сделать коробку ещё и автономной, решил внедрить плату от powerbank'a, которая включает в себя плату регулировки заряда литиевых аккумуляторов и повышения напряжения до 5 вольт, также к ней припаян бокс для аккумулятора размера 18650.

Для удобства крепления вычислительной платы и платы питания внутри корпуса в САПР Autodesk Fusion 360 были разработаны крепления, приклеенные к тыльной стороне.

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

Итог

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

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

Подробнее..

Перевод 10 инструментов для повышения продуктивности React-девелоперов в 2020 году

22.07.2020 18:16:54 | Автор: admin

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

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

Итак, взглянем на итоговый список.

ESLint



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

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

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

  • Встроенная поддержка библиотеки React (например, поддержка правил использования хуков).
  • Фокус на лучших практиках.

ESLint must-have для любого проекта, а конфигурация от Airbnb одна из моих любимых. Я кое-что добавил в нее, но 90% осталось от начальной конфигурации.

Bit



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

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

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

Create React App



Будучи React-разработчиком, вы хорошо знаете, сколько времени уходит на то, чтобы начать новый проект. Для экономии времени разработчики Facebook предложили Create React App.

Это начальный конструктор, который дает возможность сосредоточиться на разработке, а не настройке React. Все, что нужно сделать, выполнить команду npx create-react-app my-app. После чего у вас есть полностью настроенное приложение для проекта.

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

React Cosmos



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

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

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

React Toolbox



В начале статьи я упоминал, что использую Material UI для большинства проектов. Тому есть причина: мне нравится Material Design, и Material UI то, что меня полностью устраивает.

Но есть и альтернативы. Одна из них React Toolbox. Это набор часто используемых компонентов, разработанных с помощью Material Design. В нем применяются CSS-модули для большей гибкости.

На момент написания статьи React Toolbox включает 28 компонентов. Вот некоторые из них: Панель приложений, Выбор даты, Переключатель и Снэк-бар.

React Bootstrap



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

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

Решение пакет с открытым исходным кодом React Bootstrap. Все его Bootstrap-компоненты модифицированы специально для разработчиков React.

React Slingshot



Хотите ускорить разработку приложений React? Попробуйте оценить React Slingshot. Это шаблон, который объединяет React, Redux и Babel.

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

Reactide



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

Например, я нашел Reactide это IDE для React. Я был очень удивлен этой находке и попробовал инструмент.

Оказалось, что у него есть интересные особенности. Например, вы можете визуализировать компоненты вашего проекта прямо в IDE. Также есть встроенный сервер Node.js, который интегрирован с симулятором браузера.

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

React Testing Library



Сначала тесты! то, что я твержу постоянно.

Другими словами, иметь набор инструментов для тестирования важно. React Testing Library это небольшая библиотека. Она позволяет проверить код React без особых проблем. Библиотека проста в использовании и добавляет возможности стандартным библиотекам вроде react-dom и react-dom/test-utils.

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

Storybook



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

Storybook достаточно гибкий, чтобы работать с любым основным фреймворком JavaScript. Например, React, Vue.js, Angular, Svelte, Marko и даже с необработанным HTML.

Самое лучшее в Storybook то, что он поставляется с различными дополнениями. Вот ряд дополнений, которые мне кажутся интересными:

  • google-analytics (добавляет поддержку Google Analytics для компонентов);
  • jest;
  • storyshots(проверка компонентов с использованием снепшотов);
  • viewport (изменение разметки для адаптивных компонентов)


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

Перевод Разбираемся в моделях кода архитектуры x64

01.07.2020 18:14:54 | Автор: admin
Какой моделью кода мне воспользоваться? часто возникающий, но нечасто разбираемый вопрос при написании кода для архитектуры х64. Тем не менее, это довольно интересная проблема, и для понимания генерируемого компиляторами машинного кода х64 полезно иметь представление о моделях кода. Кроме того, для тех, кто беспокоится о производительности вплоть до мельчайших команд, выбор модели кода влияет и на оптимизацию.

Информация по этой теме в сети, или где бы то ни было еще, встречается редко. Самым важным из доступных ресурсов является официальный х64 ABI, скачать его можно по ссылке (далее по тексту он будет упоминаться как ABI). Часть информации также можно найти на man-страницах gcc. Задача данной статьи предоставить доступные рекомендации по теме, обсудить связанные с ней вопросы, а так же хорошими примерами через используемый в работе код продемонстрировать некоторые концепты.

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



Также смотрите нашу предыдущую публикацию на схожую тему: Как x86_x64 адресует память



Модели кода. Мотивационная часть


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

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

Таким образом, компромиссом становятся модели кода. [1] Модель кода это формальное соглашение между программистом и компилятором, в котором программист указывает свои намерения относительно размера ожидаемой программы (или программ), в которую попадет компилируемый в данный момент объектный модуль. [2] Модели кода нужны для того чтобы программист мог сказать компилятору: не волнуйся, этот объектный модуль пойдет только в небольшие программы, так что можно пользоваться быстрыми RIP-относительными режимами адресации. С другой стороны, он может сказать компилятору следующее: мы собираемся компоновать этот модуль в большие программы, так что пожалуйста используй неторопливые и безопасные абсолютные режимы адресации с полным 64-битным сдвигом.

О чем расскажет эта статья


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

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

Исходный пример на С


Для демонстрации обсуждаемых в этой статье концептов я воспользуюсь представленной ниже программой на С и скомпилирую ее с различными моделями кода. Как можно видеть, функция main получает доступ к четырем разным глобальным массивам и одной глобальной функции. Массивы отличаются двумя параметрами: размером и видимостью. Размер важен для объяснения средней модели кода и не понадобится в работе с малой и большой моделями. Видимость важна для работы PIC-моделей кода и бывает либо статичной (видимость только в исходном файле), либо глобальной (видимость всем скомпонованным в программу объектам).

int global_arr[100] = {2, 3};static int static_arr[100] = {9, 7};int global_arr_big[50000] = {5, 6};static int static_arr_big[50000] = {10, 20};int global_func(int param){    return param * 10;}int main(int argc, const char* argv[]){    int t = global_func(argc);    t += global_arr[7];    t += static_arr[7];    t += global_arr_big[7];    t += static_arr_big[7];    return t;}

gcc использует модель кода как значение опции -mcmodel. Кроме того, флагом -fpic можно задать PIC компиляцию.

Пример компиляции в объектный модуль через большую модель кода с использованием PIC:

> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o

Малая модель кода


Перевод цитаты из man gcc на тему малой модели кода:

-mcmodel=small
Генерация кода для малой модели: программа и ее символы должны быть скомпонованы в нижних двух гигабайтах адресного пространства. Размер указателей 64 бит. Программы могут быть скомпонованы и статически, и динамически. Это основная модель кода.


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

> objdump -dS codemodel1_small.o[...]int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: e8 00 00 00 00          callq  33 <main+0x1e>  33: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  3c: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  45: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  48: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  4e: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  51: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  57: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  5a: 8b 45 fc                mov    -0x4(%rbp),%eax}  5d: c9                      leaveq  5e: c3                      retq

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

> readelf -r codemodel1_small.oRelocation section '.rela.text' at offset 0x62bd8 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend00000000002f  001500000002 R_X86_64_PC32     0000000000000000 global_func - 4000000000038  001100000002 R_X86_64_PC32     0000000000000000 global_arr + 18000000000041  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b800000000004a  001200000002 R_X86_64_PC32     0000000000000340 global_arr_big + 18000000000053  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098

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

  t += global_arr[7];36:       8b 05 00 00 00 00       mov    0x0(%rip),%eax3c:       01 45 fc                add    %eax,-0x4(%rbp)

RIP-относительная адресация относительна очередной команде, таким образом сдвиг необходимо запатчить в команду mov таким образом, чтобы он соответствовал 0х3с. Нас интересует вторая релокация, R_X86_64_PC32, она указывает на операнд mov по адресу 0x38 и означает следующее: берем значение символа, добавляем слагаемое и вычитаем указываемый релокацией сдвиг. Если вы все корректно посчитали, вы увидите как результат разместит относительный сдвиг между очередной командой и global_arr, плюс 0х1с. Поскольку 0х1с означает седьмой int в массиве (в архитектуре х64 размер каждого int составляет 4 байта), то этот относительный сдвиг нам и нужен. Таким образом, используя RIP-относительную адресацию, команда корректно ссылается на global_arr[7].

Также интересно отметить следующее: пусть команды доступа к static_arr здесь и схожи, его переадресация использует другой символ, тем самым вместо конкретного символа указывая в секцию .data. Виной тому действия компоновщика, он размещает статический массив в известном месте секции, и таким образом массив нельзя использовать совместно с другими общими библиотеками. В итоге компоновщик урегулирует ситуацию с этой релокацией. С другой стороны, поскольку global_arr может быть использован (или перезаписан) другой общей библиотекой, уже динамический загрузчик должен будет разобраться со ссылкой к global_arr. [3]

Наконец, давайте взглянем на отсылку к global_func:

  int t = global_func(argc);24:       8b 45 ec                mov    -0x14(%rbp),%eax27:       89 c7                   mov    %eax,%edi29:       b8 00 00 00 00          mov    $0x0,%eax2e:       e8 00 00 00 00          callq  33 <main+0x1e>33:       89 45 fc                mov    %eax,-0x4(%rbp)

Поскольку операнд callq тоже RIP-относителен, релокация R_X86_64_PC32 работает здесь аналогично размещению фактического относительного сдвига к global_func в операнд.

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

Большая модель кода


Перевод цитаты из man gcc на тему большой модели кода:

-mcmodel=large
Генерация кода для большой модели: Эта модель не делает предположений относительно адресов и размеров секций.

Пример дизассемблированного кода main, скомпилированного при помощи не-PIC большой модели:

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: 48 ba 00 00 00 00 00    movabs $0x0,%rdx  35: 00 00 00  38: ff d2                   callq  *%rdx  3a: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  3d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  44: 00 00 00  47: 8b 40 1c                mov    0x1c(%rax),%eax  4a: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  4d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  54: 00 00 00  57: 8b 40 1c                mov    0x1c(%rax),%eax  5a: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  5d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  64: 00 00 00  67: 8b 40 1c                mov    0x1c(%rax),%eax  6a: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  6d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  74: 00 00 00  77: 8b 40 1c                mov    0x1c(%rax),%eax  7a: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  7d: 8b 45 fc                mov    -0x4(%rbp),%eax}  80: c9                      leaveq  81: c3                      retq

И вновь полезно взглянуть на релокации:

Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend000000000030  001500000001 R_X86_64_64       0000000000000000 global_func + 000000000003f  001100000001 R_X86_64_64       0000000000000000 global_arr + 000000000004f  000300000001 R_X86_64_64       0000000000000000 .data + 1a000000000005f  001200000001 R_X86_64_64       0000000000000340 global_arr_big + 000000000006f  000300000001 R_X86_64_64       0000000000000000 .data + 31080

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

  t += global_arr[7];3d:       48 b8 00 00 00 00 00    movabs $0x0,%rax44:       00 00 0047:       8b 40 1c                mov    0x1c(%rax),%eax4a:       01 45 fc                add    %eax,-0x4(%rbp)

Двум командам необходимо получить желаемое значение из массива. Первая команда размещает абсолютный 64-битный адрес в rax, который, как мы скоро увидим, окажется адресом global_arr, тогда как вторая команда загружает слово из (rax) + 0х1с в eax.

Так что давайте сфокусируемся на команде по адресу 0x3d, movabs, абсолютной 64-битной версии mov в архитектуре х64. Она может забросить полную 64-битную константу прямо в регистр, и так как в нашем дизассемблированном коде значение этой константы равно нулю, за ответом нам придется обратиться к таблице релокаций. В ней мы найдем абсолютную релокацию R_X86_64_64 для операнда по адресу 0x3f, со следующим значением: размещение значения символа плюс слагаемого обратно в сдвиг. Другими словами, rax будет содержать абсолютный адрес global_arr.

А что насчет функции вызова?

  int t = global_func(argc);24:       8b 45 ec                mov    -0x14(%rbp),%eax27:       89 c7                   mov    %eax,%edi29:       b8 00 00 00 00          mov    $0x0,%eax2e:       48 ba 00 00 00 00 00    movabs $0x0,%rdx35:       00 00 0038:       ff d2                   callq  *%rdx3a:       89 45 fc                mov    %eax,-0x4(%rbp)

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

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

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

Средняя модель кода


Как и ранее, давайте взглянем на перевод цитаты из man gcc:

-mcmodel=medium
Генерация кода для средней модели: Программа скомпонована в нижних двух гигабайтах адресного пространства. Здесь же расположены и малые символы. Символы размера большего, чем задано через -mlarge-data-threshold, попадают в больше данные или секции bss и могут находиться выше двух гигабайт. Программы могут быть скомпонованы и статически, и динамически.

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

Также важно отметить, что при работе со средней моделью кода для больших данных по аналогии с секциями .data и .bss создаются специальные секции: .ldata и .lbss. Это не так важно в призме темы текущей статьи, однако я собираюсь немного от нее отклониться. Детальнее с данным вопросом можно ознакомиться в ABI.

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

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: e8 00 00 00 00          callq  33 <main+0x1e>  33: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  3c: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  45: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  48: 48 b8 00 00 00 00 00    movabs $0x0,%rax  4f: 00 00 00  52: 8b 40 1c                mov    0x1c(%rax),%eax  55: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  58: 48 b8 00 00 00 00 00    movabs $0x0,%rax  5f: 00 00 00  62: 8b 40 1c                mov    0x1c(%rax),%eax  65: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  68: 8b 45 fc                mov    -0x4(%rbp),%eax}  6b: c9                      leaveq  6c: c3                      retq

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

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

Малая PIC-модель кода


Теперь давайте посмотрим на PIC варианты моделей кода, и как и раньше мы начнем с малой модели. [5] Ниже можно видеть пример кода, скомпилированного через малую PIC-модель:

int main(int argc, const char* argv[]){  15:   55                      push   %rbp  16:   48 89 e5                mov    %rsp,%rbp  19:   48 83 ec 20             sub    $0x20,%rsp  1d:   89 7d ec                mov    %edi,-0x14(%rbp)  20:   48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24:   8b 45 ec                mov    -0x14(%rbp),%eax  27:   89 c7                   mov    %eax,%edi  29:   b8 00 00 00 00          mov    $0x0,%eax  2e:   e8 00 00 00 00          callq  33 <main+0x1e>  33:   89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  3d:   8b 40 1c                mov    0x1c(%rax),%eax  40:   01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  43:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  49:   01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  4c:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  53:   8b 40 1c                mov    0x1c(%rax),%eax  56:   01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  59:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  5f:   01 45 fc                add    %eax,-0x4(%rbp)    return t;  62:   8b 45 fc                mov    -0x4(%rbp),%eax}  65:   c9                      leaveq  66:   c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend00000000002f  001600000004 R_X86_64_PLT32    0000000000000000 global_func - 4000000000039  001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4000000000045  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b800000000004f  001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 400000000005b  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098

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

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

Интересно обратить внимание на глобальные массивы: стоит напомнить, что в PIC глобальные данные должны проходить через GOT, поскольку в какой-то момент их могут хранить, или пользоваться ими, общие библиотеки [6]. Ниже можно видеть код для доступа к global_arr:

  t += global_arr[7];36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax3d:   8b 40 1c                mov    0x1c(%rax),%eax40:   01 45 fc                add    %eax,-0x4(%rbp)

Интересующая нас релокация это R_X86_64_GOTPCREL: позиция входа символа в GOT плюс слагаемое, минус сдвиг за применение релокации. Другими словами, в команду патчится относительный сдвиг между RIP (следующей инструкции) и зарезервированного для global_arr в GOT слота. Таким образом, в rax в команду по адресу 0x36 размещается фактический адрес global_arr. Следом за этим шагом идет сброс ссылки на адрес global_arr плюс сдвиг на его седьмой элемент в eax.

Теперь давайте взглянем на вызов функции:

  int t = global_func(argc);24:   8b 45 ec                mov    -0x14(%rbp),%eax27:   89 c7                   mov    %eax,%edi29:   b8 00 00 00 00          mov    $0x0,%eax2e:   e8 00 00 00 00          callq  33 <main+0x1e>33:   89 45 fc                mov    %eax,-0x4(%rbp)

В ней есть релокация операнда callq по адресу 0x2e, R_X86_64_PLT32: адрес PLT входа для символа плюс слагаемое, минус сдвиг за применение релокации. Другими словами, callq должен корректно вызывать PLT трамплин для global_func.

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

Большая PIC-модель кода


Дизассемблирование:

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 53                      push   %rbx  1a: 48 83 ec 28             sub    $0x28,%rsp  1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx  25: 49 bb 00 00 00 00 00    movabs $0x0,%r11  2c: 00 00 00  2f: 4c 01 db                add    %r11,%rbx  32: 89 7d dc                mov    %edi,-0x24(%rbp)  35: 48 89 75 d0             mov    %rsi,-0x30(%rbp)    int t = global_func(argc);  39: 8b 45 dc                mov    -0x24(%rbp),%eax  3c: 89 c7                   mov    %eax,%edi  3e: b8 00 00 00 00          mov    $0x0,%eax  43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx  4a: 00 00 00  4d: 48 01 da                add    %rbx,%rdx  50: ff d2                   callq  *%rdx  52: 89 45 ec                mov    %eax,-0x14(%rbp)    t += global_arr[7];  55: 48 b8 00 00 00 00 00    movabs $0x0,%rax  5c: 00 00 00  5f: 48 8b 04 03             mov    (%rbx,%rax,1),%rax  63: 8b 40 1c                mov    0x1c(%rax),%eax  66: 01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr[7];  69: 48 b8 00 00 00 00 00    movabs $0x0,%rax  70: 00 00 00  73: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  77: 01 45 ec                add    %eax,-0x14(%rbp)    t += global_arr_big[7];  7a: 48 b8 00 00 00 00 00    movabs $0x0,%rax  81: 00 00 00  84: 48 8b 04 03             mov    (%rbx,%rax,1),%rax  88: 8b 40 1c                mov    0x1c(%rax),%eax  8b: 01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr_big[7];  8e: 48 b8 00 00 00 00 00    movabs $0x0,%rax  95: 00 00 00  98: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  9c: 01 45 ec                add    %eax,-0x14(%rbp)    return t;  9f: 8b 45 ec                mov    -0x14(%rbp),%eax}  a2: 48 83 c4 28             add    $0x28,%rsp  a6: 5b                      pop    %rbx  a7: c9                      leaveq  a8: c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9
000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0
000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0
00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0
00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0
000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0

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

1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx25: 49 bb 00 00 00 00 00    movabs $0x0,%r112c: 00 00 002f: 4c 01 db                add    %r11,%rbx

Ниже можно прочесть перевод связанной с этим цитаты из ABI:

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

Давайте посмотрим на то, как описанный выше пролог вычисляет адрес GOT. Во-первых, команда по адресу 0x1e загружает свой собственный адрес в rbx. Затем совместно с релокацией R_X86_64_GOTPC64 совершается абсолютный 64-битный шаг в r11. Эта релокация означает следующее: берем адрес GOT, вычитаем перемещаемый сдвиг и добавляем слагаемое. Наконец, команда по адресу 0x2f складывает оба результата вместе. Итогом становится абсолютный адрес GOT в rbx. [7]

Зачем же мучиться с вычислением адреса GOT? Во-первых, как отмечено в цитате, в большой модели кода мы не можем предполагать, что 32-битного RIP-относительного сдвига будет достаточно для адресации GOT, из-за чего нам и требуется полный 64-битный адрес. Во-вторых, мы все еще хотим работать с PIC-вариацией, так что мы не можем попросту поместить абсолютный адрес в регистр. Скорее сам адрес должен быть вычислен относительно RIP. Для этого и нужен пролог: он совершает 64-битное RIP-относительное вычисление.

В любом случае, раз у нас в rbx теперь есть адрес GOT, давайте посмотрим на то как получить доступ к static_arr:

  t += static_arr[7];69:       48 b8 00 00 00 00 00    movabs $0x0,%rax70:       00 00 0073:       8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax77:       01 45 ec                add    %eax,-0x14(%rbp)

Релокация первой команды это R_X86_64_GOTOFF64: символ плюс слагаемое минус GOT. В нашем случае это относительный сдвиг между адресом static_arr и адресом GOT. Следующая инструкция добавляет результат в rbx (абсолютный адрес GOT) и сбрасывает ссылку со сдвигом по 0x1c. Для простоты визуализации такого вычисления ниже можно ознакомиться с псевдо-C примером:

// char* static_arr// char* GOTrax = static_arr + 0 - GOT;  // rax now contains an offseteax = *(rbx + rax + 0x1c);   // rbx == GOT, so eax now contains                             // *(GOT + static_arr - GOT + 0x1c) or                             // *(static_arr + 0x1c)

Обратите внимание на интересный момент: адрес GOT используется как привязка к static_arr. Обычно GOT не содержит в себе адрес символа, и так как static_arr не является внешним символом, нет причин хранить его внутри GOT. Тем не менее, в данном случае GOT используется как привязка к относительной адресу символа секции данных. Этот адрес, который кроме всего прочего не зависит от расположения, можно найти полным 64-битным сдвигом. Компоновщик в состоянии урегулировать эту релокацию, так что модифицировать секцию кода во время загрузки нет необходимости.

Но что насчет global_arr?

  t += global_arr[7];55:       48 b8 00 00 00 00 00    movabs $0x0,%rax5c:       00 00 005f:       48 8b 04 03             mov    (%rbx,%rax,1),%rax63:       8b 40 1c                mov    0x1c(%rax),%eax66:       01 45 ec                add    %eax,-0x14(%rbp)

Этот код несколько длиннее, а релокация отличается от обычной. По сути, GOT используется здесь более традиционным образом: релокация R_X86_64_GOT64 для movabs лишь говорит функции разместить сдвиг в GOT, там где в rax расположен адрес global_arr. Команда по адресу 0x5f берет адрес global_arr из GOT и помещает его в rax. Следующая команда сбрасывает ссылку на global_arr[7] и помещает значение в eax.

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

  int t = global_func(argc);39: 8b 45 dc                mov    -0x24(%rbp),%eax3c: 89 c7                   mov    %eax,%edi3e: b8 00 00 00 00          mov    $0x0,%eax43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx4a: 00 00 004d: 48 01 da                add    %rbx,%rdx50: ff d2                   callq  *%rdx52: 89 45 ec                mov    %eax,-0x14(%rbp)

Интересующая нас релокация это R_X86_64_PLTOFF64: адрес PLT входа для global_func минус адрес GOT. Результат размещается в rdx, куда затем помещается rbx (абсолютный адрес GOT). В итоге мы получаем адрес PLT входа для global_func в rdx.

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

Средняя PIC-модель кода


Наконец, мы разберем сгенерированный для средней PIC-модели код:

int main(int argc, const char* argv[]){  15:   55                      push   %rbp  16:   48 89 e5                mov    %rsp,%rbp  19:   53                      push   %rbx  1a:   48 83 ec 28             sub    $0x28,%rsp  1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx  25:   89 7d dc                mov    %edi,-0x24(%rbp)  28:   48 89 75 d0             mov    %rsi,-0x30(%rbp)    int t = global_func(argc);  2c:   8b 45 dc                mov    -0x24(%rbp),%eax  2f:   89 c7                   mov    %eax,%edi  31:   b8 00 00 00 00          mov    $0x0,%eax  36:   e8 00 00 00 00          callq  3b <main+0x26>  3b:   89 45 ec                mov    %eax,-0x14(%rbp)    t += global_arr[7];  3e:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  45:   8b 40 1c                mov    0x1c(%rax),%eax  48:   01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr[7];  4b:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  51:   01 45 ec                add    %eax,-0x14(%rbp)    t += global_arr_big[7];  54:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  5b:   8b 40 1c                mov    0x1c(%rax),%eax  5e:   01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr_big[7];  61:   48 b8 00 00 00 00 00    movabs $0x0,%rax  68:   00 00 00  6b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  6f:   01 45 ec                add    %eax,-0x14(%rbp)    return t;  72:   8b 45 ec                mov    -0x14(%rbp),%eax}  75:   48 83 c4 28             add    $0x28,%rsp  79:   5b                      pop    %rbx  7a:   c9                      leaveq  7b:   c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend000000000021  00160000001a R_X86_64_GOTPC32  0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4000000000037  001700000004 R_X86_64_PLT32    0000000000000000 global_func - 4000000000041  001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 400000000004d  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8000000000057  001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4000000000063  000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0

Для начала давайте уберем вызов функции. Аналогично малой модели, в средней модели мы предполагаем, что ссылки на код не превышают пределов 32-битного RIP сдвига, следовательно, код для вызова global_func полностью аналогичен такому же коду в малой PIC-модели, равно как и для случаев массивов малых данных static_arr и global_arr. Поэтому мы сфокусируемся на массивах больших данных, но сначала поговорим о прологе: здесь он отличается от пролога большой модели данных.

1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx

Это весь пролог: чтобы при помощи релокации R_X86_64_GOTPC32 разместить GOT адрес в rbx, потребовалась всего одна команда (по сравнению с тремя в большой модели). В чем же разница? Дело в том, что так как в средней модели GOT не является частью секций больших данных, мы предполагаем его доступность в рамках 32-битного сдвига. В большой модели мы не могли совершать подобные предположения, и были вынуждены пользоваться полным 64-битным сдвигом.

Вызывает интерес тот факт, что код для доступа к global_arr_big похож на такой же код в малой PIC-модели. Это происходит по той же причине, почему пролог средней модели короче пролога большой модели: мы полагаем доступность GOT в рамках 32-битной RIP-относительной адресации. Действительно, к самому global_arr_big нельзя получить такой доступ, но этот случай все равно покрывает GOT, так как фактически global_arr_big в нем и находится, причем в виде полного 64-битного адреса.

Ситуация, тем не менее, отличается для static_arr_big:

  t += static_arr_big[7];61:   48 b8 00 00 00 00 00    movabs $0x0,%rax68:   00 00 006b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax6f:   01 45 ec                add    %eax,-0x14(%rbp)

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

Примечания:


[1] Не стоит путать модели кода с 64-битными моделями данных и моделями памяти Intel, все это разные темы.

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

[3] Если что-то осталось непонятным, ознакомьтесь со следующей статьей.

[4] Тем не менее, объемы постепенно растут. Когда я в последний раз проверял Debug+Asserts билд Clang, он почти достигал одного гигабайта, за что во многом спасибо автогенерируемому коду.

[5] Если вы еще не знаете как работает PIC (как в целом, так и в частности для архитектуры x64), самое время ознакомиться со следующими статьями по теме: раз и два.

[6] Таким образом, компоновщик не может самостоятельно разрешить ссылки, и вынужден переложить обработку GOT на динамический загрузчик.

[7] 0x25 0x7 + GOT 0x27 + 0x9 = GOT



Подробнее..

Когда у вас сберовские масштабы. Использование Ab Initio при работе с Hive и GreenPlum

07.07.2020 18:11:11 | Автор: admin
Некоторое время назад перед нами встал вопрос выбора ETL-средства для работы с BigData. Ранее использовавшееся решение Informatica BDM не устраивало нас из-за ограниченной функциональности. Её использование свелось к фреймворку по запуску команд spark-submit. На рынке имелось не так много аналогов, в принципе способных работать с тем объёмом данных, с которым мы имеем дело каждый день. В итоге мы выбрали Ab Initio. В ходе пилотных демонстраций продукт показал очень высокую скорость обработки данных. Информации об Ab Initio на русском языке почти нет, поэтому мы решили рассказать о своём опыте на Хабре.

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

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

В посте я расскажу о возможностях Ab Initio и приведу сравнительные характеристики по его работе с Hive и GreenPlum.

  • Описание фреймворка MDW и работ по его донастройке под GreenPlum
  • Сравнительные характеристики производительности Ab Initio по работе с Hive и GreenPlum
  • Работа Ab Initio с GreenPlum в режиме Near Real Time


Функционал этого продукта очень широк и требует немало времени на своё изучение. Однако, при должных навыках работы и правильных настройках производительности результаты обработки данных получаются весьма впечатляющие. Использование Ab Initio для разработчика может дать ему интересный опыт. Это новый взгляд на ETL-разработку, гибрид между визуальной средой и разработкой загрузок на скрипто-подобном языке.
Бизнес развивает свои экосистемы и этот инструмент оказывается ему как никогда кстати. С помощью Ab Initio можно копить знания о текущем бизнесе и использовать эти знания для расширения старых и открытия новых бизнесов. Альтернативами Ab Initio можно назвать из визуальных сред разработки Informatica BDM и из невизуальных сред Apache Spark.

Описание Ab Initio


Ab Initio, как и другие ETL-средства, представляет собой набор продуктов.

Ab Initio GDE (Graphical Development Environment) это среда для разработчика, в которой он настраивает трансформации данных и соединяет их потоками данных в виде стрелочек. При этом такой набор трансформаций называется графом:

Входные и выходные соединения функциональных компонентов являются портами и содержат поля, вычисленные внутри преобразований. Несколько графов, соединённых потоками в виде стрелочек в порядке их выполнения называются планом.
Имеется несколько сотен функциональных компонентов, что очень много. Многие из них узкоспециализированные. Возможности классических трансформаций в Ab Initio шире, чем в других ETL-средствах. Например, Join имеет несколько выходов. Помимо результата соединения датасетов можно получить на выходе записи входных датасетов, по ключам которых не удалось соединиться. Также можно получить rejects, errors и лог работы трансформации, который можно в этом же графе прочитать как текстовый файл и обработать другими трансформациями:

Или, например, можно материализовать приёмник данных в виде таблицы и в этом же графе считать из него данные.
Есть оригинальные трансформации. Например, трансформация Scan имеет функционал, как у аналитических функций. Есть трансформации с говорящими названиями: Create Data, Read Excel, Normalize, Sort within Groups, Run Program, Run SQL, Join with DB и др. Графы могут использовать параметры времени выполнения, в том числе возможна передача параметров из операционной системы или в операционную систему. Файлы с готовым набором передаваемых графу параметров называются parameter sets (psets).
Как и полагается, Ab Initio GDE имеет свой репозиторий, именуемый EME (Enterprise Meta Environment). Разработчики имеют возможность работать с локальными версиями кода и делать check in своих разработок в центральный репозиторий.
Имеется возможность во время выполнения или после выполнения графа кликнуть по любому соединяющему трансформации потоку и посмотреть на данные, прошедшие между этими трансформациями:

Также есть возможность кликнуть по любому потоку и посмотреть tracking details в сколько параллелей работала трансформация, сколько строк и байт в какой из параллелей загрузилось:

Есть возможность разбить выполнение графа на фазы и пометить, что одни трансформации нужно выполнять первым делом (в нулевой фазе), следующие в первой фазе, следующие во второй фазе и т.д.
У каждой трансформации можно выбрать так называемый layout (где она будет выполняться): без параллелей или в параллельных потоках, число которых можно задать. При этом временные файлы, которые создаёт Ab Initio при работе трансформаций, можно размещать как в файловой системе сервера, так и в HDFS.
В каждой трансформации на базе шаблона по умолчанию можно создать свой скрипт на языке PDL, который немного напоминает shell.
С помощью языка PDL вы можете расширять функционал трансформаций и, в частности, вы можете динамически (во время выполнения) генерировать произвольные фрагменты кода в зависимости от параметров времени выполнения.
Также в Ab Initio хорошо развита интеграция с ОС через shell. Конкретно в Сбербанке используется linux ksh. Можно обмениваться с shell переменными и использовать их в качестве параметров графов. Можно из shell вызывать выполнение графов Ab Initio и администрировать Ab Initio.
Помимо Ab Initio GDE в поставку входит много других продуктов. Есть своя Co>Operation System с претензией называться операционной системой. Есть Control>Center, в котором можно ставить на расписание и мониторить потоки загрузки. Есть продукты для осуществления разработки на более примитивном уровне, чем позволяет Ab Initio GDE.

Описание фреймворка MDW и работ по его донастройке под GreenPlum


Вместе со своими продуктами вендор поставляет продукт MDW (Metadata Driven Warehouse), который представляет собой конфигуратор графов, предназначенный для помощи в типичных задачах по наполнению хранилищ данных или data vaults.
Он содержит пользовательские (специфичные для проекта) парсеры метаданных и готовые генераторы кода из коробки.

На входе MDW получает модель данных, конфигурационный файл по настройке соединения с базой данных (Oracle, Teradata или Hive) и некоторые другие настройки. Специфическая для проекта часть, например, разворачивает модель в базе данных. Коробочная часть продукта генерирует графы и настроечные файлы к ним по загрузке данных в таблицы модели. При этом создаются графы (и psets) для нескольких режимов инициализирующей и инкрементальной работы по обновлению сущностей.
В случаях Hive и RDBMS генерируются различающиеся графы по инициализирующему и инкрементальному обновлению данных.
В случае Hive поступившие данные дельты соединяется посредством Ab Initio Join с данными, которые были в таблице до обновления. Загрузчики данных в MDW (как в Hive, так и в RDBMS) не только вставляют новые данные из дельты, но и закрывают периоды актуальности данных, по первичным ключам которых поступила дельта. Кроме того, приходится переписать заново неизменившуюся часть данных. Но так приходится делать, поскольку в Hive нет операций delete или update.

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

Поступившая дельта загружается в промежуточную таблицу в базу данных. После этого происходит соединение дельты с данными, которые были в таблице до обновления. И делается это силами SQL посредством сгенерированного SQL-запроса. Далее с помощью SQL-команд delete+insert в целевую таблицу происходит вставка новых данных из дельты и закрытие периодов актуальности данных, по первичным ключам которых поступила дельта. Неизменившиеся данные переписывать нет нужды.
Таким образом, мы пришли к выводу, что в случае Hive MDW должен пойти на переписывание всей таблицы, потому что Hive не имеет функции обновления. И ничего лучше полного переписывания данных при обновлении не придумано. В случае же RDBMS, наоборот, создатели продукта сочли нужным доверить соединение и обновление таблиц использованию SQL.
Для проекта в Сбербанке мы создали новую многократно используемую реализацию загрузчика базы данных для GreenPlum. Сделано это было на основе версии, которую MDW генерирует для Teradata. Именно Teradata, а не Oracle подошла для этого лучше и ближе всего, т.к. тоже является MPP-системой. Способы работы, а также синтаксис Teradata и GreenPlum оказались близки.
Примеры критичных для MDW различий между разными RDBMS таковы. В GreenPlum в отличии от Teradata при создании таблиц нужно писать клаузу
distributed by

В Teradata пишут
delete <table> all

, а в GreеnPlum пишут
delete from <table>

В Oracle в целях оптимизации пишут
delete from t where rowid in (<соединение t с дельтой>)

, а в Teradata и GreenPlum пишут
delete from t where exists (select * from delta where delta.pk=t.pk)

Ещё отметим, что для работы Ab Initio с GreenPlum потребовалось установить клиент GreenPlum на все ноды кластера Ab Initio. Это потому, что мы подключились к GreenPlum одновременно со всех узлов нашего кластера. А для того, чтобы чтение из GreenPlum было параллельным и каждый параллельный поток Ab Initio читал свою порцию данных из GreenPlum, пришлось в секцию where SQL-запросов поместить понимаемую Ab Initio конструкцию
where ABLOCAL()

и определить значение этой конструкции, указав читающей из БД трансформации параметр
ablocal_expr=string_concat("mod(t.", string_filter_out("{$TABLE_KEY}","{}"), ",", (decimal(3))(number_of_partitions()),")=", (decimal(3))(this_partition()))

, которая компилируется в что-то типа
mod(sk,10)=3

, т.е. приходится подсказывать GreenPlum явный фильтр для каждой партиции. Для других баз данных (Teradata, Oracle) Ab Initio может выполнить это распараллеливание автоматически.

Сравнительные характеристики производительности Ab Initio по работе с Hive и GreenPlum


В Сбербанке был проведён эксперимент по сравнению производительности сгенерированных MDW графов применительно к Hive и применительно к GreenPlum. В рамках эксперимента в случае Hive имелось 5 нод на том же кластере, что и Ab Initio, а в случае GreenPlum имелось 4 ноды на отдельном кластере. Т.е. Hive имел некоторое преимущество над GreenPlum по железу.
Было рассмотрено две пары графов, выполняющих одну и ту же задачу обновления данных в Hive и в GreenPlum. При этом запускали графы, сгенерированные конфигуратором MDW:

  • инициализирующая загрузка + инкрементальная загрузка случайно сгенерированных данных в таблицу Hive
  • инициализирующая загрузка + инкрементальная загрузка случайно сгенерированных данных в такую же таблицу GreenPlum

В обоих случаях (Hive и GreenPlum) запускали загрузки в 10 параллельных потоков на одном и том же кластере Ab Initio. Промежуточные данные для расчётов Ab Initio сохранял в HDFS (в терминах Ab Initio был использован MFS layout using HDFS). Одна строка случайно сгенерированных данных занимала в обоих случаях по 200 байт.
Результат получился такой:

Hive:
Инициализирующая загрузка в Hive
Вставлено строк 6 000 000 60 000 000 600 000 000
Продолжительность инициализирующей
загрузки в секундах
41 203 1 601
Инкрементальная загрузка в Hive
Количество строк, имевшихся в
целевой таблице на начало эксперимента
6 000 000 60 000 000 600 000 000
Количество строк дельты, применённых к
целевой таблице в ходе эксперимента
6 000 000 6 000 000 6 000 000
Продолжительность инкрементальной
загрузки в секундах
88 299 2 541

GreenPlum:
Инициализирующая загрузка в GreenPlum
Вставлено строк 6 000 000 60 000 000 600 000 000
Продолжительность инициализирующей
загрузки в секундах
72 360 3 631
Инкрементальная загрузка в GreenPlum
Количество строк, имевшихся в
целевой таблице на начало эксперимента
6 000 000 60 000 000 600 000 000
Количество строк дельты, применённых к
целевой таблице в ходе эксперимента
6 000 000 6 000 000 6 000 000
Продолжительность инкрементальной
загрузки в секундах
159 199 321

Видим, что скорость инициализирующей загрузки как в Hive, так и в GreenPlum линейно зависит от объёма данных и по причинам лучшего железа она несколько быстрее для Hive, чем для GreenPlum.
Инкрементальная загрузка в Hive также линейно зависит от объёма имеющихся в целевой таблице ранее загруженных данных и проходит достаточно медленно с ростом объёма. Вызвано это необходимостью перезаписывать целевую таблицу полностью. Это означает, что применение маленьких изменений к огромным таблицам не очень хороший вариант использования для Hive.
Инкрементальная же загрузка в GreenPlum слабо зависит от объёма имеющихся в целевой таблице ранее загруженных данных и проходит достаточно быстро. Получилось это благодаря SQL Joins и архитектуре GreenPlum, допускающей операцию delete.

Итак, GreenPlum вливает дельту методом delete+insert, а в Hive нету операций delete либо update, поэтому весь массив данных при инкрементальном обновлении были вынуждены переписывать целиком. Наиболее показательно сравнение выделенных жирным ячеек, так как оно соответствует наиболее частому варианту эксплуатации ресурсоёмких загрузок. Видим, что GreenPlum выиграл у Hive в этом тесте в 8 раз.

Работа Ab Initio с GreenPlum в режиме Near Real Time


В этом эксперименте проверим возможность Ab Initio производить обновление таблицы GreenPlum случайно формируемыми порциями данных в режиме, близком к реальному времени. Рассмотрим таблицу GreenPlum dev42_1_db_usl.TESTING_SUBJ_org_finval, с которой будет вестись работа.
Будем использовать три графа Ab Initio по работе с ней:

1) Граф Create_test_data.mp создаёт в 10 параллельных потоков файлы с данными в HDFS на 6 000 000 строк. Данные случайные, структура их организована для вставки в нашу таблицу



2) Граф mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset сгенерированный MDW граф по инициализирующей вставке данных в нашу таблицу в 10 параллельных потоков (используются тестовые данные, сгенерированные графом (1))


3) Граф mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset сгенерированный MDW граф по инкрементальному обновлению нашей таблицы в 10 параллельных потоков с использованием порции свежих поступивших данных (дельты), сгенерированных графом (1)


Выполним нижеприведённый сценарий в режиме NRT:

  • сгенерировать 6 000 000 тестовых строк
  • произвести инициализирующую загрузку вставить 6 000 000 тестовых строк в пустую таблицу
  • повторить 5 раз инкрементальную загрузку
    • сгенерировать 6 000 000 тестовых строк
    • произвести инкрементальную вставку 6 000 000 тестовых строк в таблицу (при этом старым данным проставляется время истечения актуальности valid_to_ts и вставляются более свежие данные с тем же первичным ключом)

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

Теперь посмотрим лог работы сценария:
Start Create_test_data.input.pset at 2020-06-04 11:49:11
Finish Create_test_data.input.pset at 2020-06-04 11:49:37
Start mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:49:37
Finish mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:50:42
Start Create_test_data.input.pset at 2020-06-04 11:50:42
Finish Create_test_data.input.pset at 2020-06-04 11:51:06
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:51:06
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:53:41
Start Create_test_data.input.pset at 2020-06-04 11:53:41
Finish Create_test_data.input.pset at 2020-06-04 11:54:04
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:54:04
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:56:51
Start Create_test_data.input.pset at 2020-06-04 11:56:51
Finish Create_test_data.input.pset at 2020-06-04 11:57:14
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:57:14
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:59:55
Start Create_test_data.input.pset at 2020-06-04 11:59:55
Finish Create_test_data.input.pset at 2020-06-04 12:00:23
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 12:00:23
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 12:03:23
Start Create_test_data.input.pset at 2020-06-04 12:03:23
Finish Create_test_data.input.pset at 2020-06-04 12:03:49
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 12:03:49
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 12:06:46


Получается такая картина:
Graph Start time Finish time Length
Create_test_data.input.pset 04.06.2020 11:49:11 04.06.2020 11:49:37 00:00:26
mdw_load.day_one.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11:49:37 04.06.2020 11:50:42 00:01:05
Create_test_data.input.pset 04.06.2020 11:50:42 04.06.2020 11:51:06 00:00:24
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11:51:06 04.06.2020 11:53:41 00:02:35
Create_test_data.input.pset 04.06.2020 11:53:41 04.06.2020 11:54:04 00:00:23
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11:54:04 04.06.2020 11:56:51 00:02:47
Create_test_data.input.pset 04.06.2020 11:56:51 04.06.2020 11:57:14 00:00:23
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11:57:14 04.06.2020 11:59:55 00:02:41
Create_test_data.input.pset 04.06.2020 11:59:55 04.06.2020 12:00:23 00:00:28
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 12:00:23 04.06.2020 12:03:23 00:03:00
Create_test_data.input.pset 04.06.2020 12:03:23 04.06.2020 12:03:49 00:00:26
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 12:03:49 04.06.2020 12:06:46 00:02:57

Видим, что 6 000 000 строк инкремента обрабатываются за 3 минуты, что достаточно быстро.
Данные в целевой таблице получились распределёнными следующим образом:
select valid_from_ts, valid_to_ts, count(1), min(sk), max(sk) from dev42_1_db_usl.TESTING_SUBJ_org_finval group by valid_from_ts, valid_to_ts order by 1,2;


Можно разглядеть соответствие вставленных данных моментам запуска графов.
Значит можно запускать в Ab Initio инкрементальную загрузку данных в GreenPlum с очень высокой частотой и наблюдать высокую скорость вставки этих данных в GreenPlum. Конечно, раз в секунду запускаться не получится, так как Ab Initio, как и любое ETL-средство, при запуске требует времени на раскачку.

Заключение


Сейчас Ab Initio используется в Сбербанке для построения Единого семантического слоя данных (ЕСС). Этот проект подразумевает построение единой версии состояния различных банковских бизнес-сущностей. Информация приходит из различных источников, реплики которых готовятся на Hadoop. Исходя из потребностей бизнеса, готовится модель данных и описываются трансформации данных. Ab Initio загружает информацию в ЕСС и загруженные данные не только представляют интерес для бизнеса сами по себе, но и служат источником для построения витрин данных. При этом функционал продукта позволяет использовать в качестве приёмника различные системы (Hive, Greenplum, Teradata, Oracle), что даёт возможность без особых усилий подготавливать данные для бизнеса в различных требуемых ему форматах.

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

Автор эксперт профессионального сообщества Сбербанка SberProfi DWH/BigData. Профессиональное сообщество SberProfi DWH/BigData отвечает за развитие компетенций в таких направлениях, как экосистема Hadoop, Teradata, Oracle DB, GreenPlum, а также BI инструментах Qlik, SAP BO, Tableau и др.
Подробнее..

10 приёмов работы с Oracle

01.10.2020 10:13:11 | Автор: admin
В Сбере есть несколько практик Oracle, которые могут оказаться вам полезны. Думаю, часть вам знакома, но мы используем для загрузки не только ETL-средства, но и хранимые процедуры Oracle. На Oracle PL/SQL реализованы наиболее сложные алгоритмы загрузки данных в хранилища, где требуется прочувствовать каждый байт.

  • Автоматическое журналирование компиляций
  • Как быть, если хочется сделать вьюшку с параметрами
  • Использование динамической статистики в запросах
  • Как сохранить план запроса при вставке данных через database link
  • Запуск процедур в параллельных сессиях
  • Протягивание остатков
  • Объединение нескольких историй в одну
  • Нормалайзер
  • Визуализация в формате SVG
  • Приложение поиска по метаданным Oracle


Автоматическое журналирование компиляций


На некоторых базах данных Oracle в Сбере стоит триггер на компиляцию, который запоминает, кто, когда и что менял в коде серверных объектов. Тем самым из таблицы журнала компиляций можно установить автора изменений. Также автоматически реализуется система контроля версий. Во всяком случае, если программист забыл сдать изменения в Git, то этот механизм его подстрахует. Опишем пример реализации такой системы автоматического журналирования компиляций. Один из упрощённых вариантов триггера на компиляцию, пишущего в журнал в виде таблицы ddl_changes_log, выглядит так:

create table DDL_CHANGES_LOG(  id               INTEGER,  change_date      DATE,  sid              VARCHAR2(100),  schemaname       VARCHAR2(30),  machine          VARCHAR2(100),  program          VARCHAR2(100),  osuser           VARCHAR2(100),  obj_owner        VARCHAR2(30),  obj_type         VARCHAR2(30),  obj_name         VARCHAR2(30),  previous_version CLOB,  changes_script   CLOB);create or replace trigger trig_audit_ddl_trg  before ddl on databasedeclare  v_sysdate              date;  v_valid                number;  v_previous_obj_owner   varchar2(30) := '';  v_previous_obj_type    varchar2(30) := '';  v_previous_obj_name    varchar2(30) := '';  v_previous_change_date date;  v_lob_loc_old          clob := '';  v_lob_loc_new          clob := '';  v_n                    number;  v_sql_text             ora_name_list_t;  v_sid                  varchar2(100) := '';  v_schemaname           varchar2(30) := '';  v_machine              varchar2(100) := '';  v_program              varchar2(100) := '';  v_osuser               varchar2(100) := '';begin  v_sysdate := sysdate;  -- find whether compiled object already presents and is valid  select count(*)    into v_valid    from sys.dba_objects   where owner = ora_dict_obj_owner     and object_type = ora_dict_obj_type     and object_name = ora_dict_obj_name     and status = 'VALID'     and owner not in ('SYS', 'SPOT', 'WMSYS', 'XDB', 'SYSTEM')     and object_type in ('TRIGGER', 'PROCEDURE', 'FUNCTION', 'PACKAGE', 'PACKAGE BODY', 'VIEW');  -- find information about previous compiled object  select max(obj_owner) keep(dense_rank last order by id),         max(obj_type) keep(dense_rank last order by id),         max(obj_name) keep(dense_rank last order by id),         max(change_date) keep(dense_rank last order by id)    into v_previous_obj_owner, v_previous_obj_type, v_previous_obj_name, v_previous_change_date    from ddl_changes_log;  -- if compile valid object or compile invalid package body broken by previous compilation of package then log it  if (v_valid = 1 or v_previous_obj_owner = ora_dict_obj_owner and     (v_previous_obj_type = 'PACKAGE' and ora_dict_obj_type = 'PACKAGE BODY' or     v_previous_obj_type = 'PACKAGE BODY' and ora_dict_obj_type = 'PACKAGE') and     v_previous_obj_name = ora_dict_obj_name and     v_sysdate - v_previous_change_date <= 1 / 24 / 60 / 2) and     ora_sysevent in ('CREATE', 'ALTER') then    -- store previous version of object (before compilation) from dba_source or dba_views in v_lob_loc_old    if ora_dict_obj_type <> 'VIEW' then      for z in (select substr(text, 1, length(text) - 1) || chr(13) || chr(10) as text                  from sys.dba_source                 where owner = ora_dict_obj_owner                   and type = ora_dict_obj_type                   and name = ora_dict_obj_name                 order by line) loop        v_lob_loc_old := v_lob_loc_old || z.text;      end loop;    else      select sys.dbms_metadata_util.long2clob(v.textlength, 'SYS.VIEW$', 'TEXT', v.rowid) into v_lob_loc_old        from sys."_CURRENT_EDITION_OBJ" o, sys.view$ v, sys.user$ u       where o.obj# = v.obj#         and o.owner# = u.user#         and u.name = ora_dict_obj_owner         and o.name = ora_dict_obj_name;    end if;    -- store new version of object (after compilation) from v_sql_text in v_lob_loc_new    v_n := ora_sql_txt(v_sql_text);    for i in 1 .. v_n loop      v_lob_loc_new := v_lob_loc_new || replace(v_sql_text(i), chr(10), chr(13) || chr(10));    end loop;    -- find information about session that changed this object    select max(to_char(sid)), max(schemaname), max(machine), max(program), max(osuser)      into v_sid, v_schemaname, v_machine, v_program, v_osuser      from v$session     where audsid = userenv('sessionid');    -- store changes in ddl_changes_log    insert into ddl_changes_log      (id, change_date, sid, schemaname, machine, program, osuser,       obj_owner, obj_type, obj_name, previous_version, changes_script)    values      (seq_ddl_changes_log.nextval, v_sysdate, v_sid, v_schemaname, v_machine, v_program, v_osuser,       ora_dict_obj_owner, ora_dict_obj_type, ora_dict_obj_name, v_lob_loc_old, v_lob_loc_new);  end if;exception  when others then    null;end;

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

Как быть, если хочется сделать вьюшку с параметрами


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

create table DIVISION_SALES(  division_id INTEGER,  dt          DATE,  sales_amt   NUMBER);

Такой запрос сравнивает продажи по подразделениям за два дня. В данном случае, 30.04.2020 и 11.09.2020.

select t1.division_id,       t1.dt          dt1,       t2.dt          dt2,       t1.sales_amt   sales_amt1,       t2.sales_amt   sales_amt2  from (select dt, division_id, sales_amt          from division_sales         where dt = to_date('30.04.2020', 'dd.mm.yyyy')) t1,       (select dt, division_id, sales_amt          from division_sales         where dt = to_date('11.09.2020', 'dd.mm.yyyy')) t2 where t1.division_id = t2.division_id;

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

create or replace view vw_division_sales_report(in_dt1 date, in_dt2 date) asselect t1.division_id,       t1.dt          dt1,       t2.dt          dt2,       t1.sales_amt   sales_amt1,       t2.sales_amt   sales_amt2  from (select dt, division_id, sales_amt          from division_sales         where dt = in_dt1) t1,       (select dt, division_id, sales_amt          from division_sales         where dt = in_dt2) t2 where t1.division_id = t2.division_id;

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

create type t_division_sales_report as object(  division_id INTEGER,  dt1         DATE,  dt2         DATE,  sales_amt1  NUMBER,  sales_amt2  NUMBER);

И создадим тип под таблицу из таких строк.

create type t_division_sales_report_table as table of t_division_sales_report;

Вместо вьюшки напишем pipelined функцию с входными параметрами-датами.

create or replace function func_division_sales(in_dt1 date, in_dt2 date)  return t_division_sales_report_table  pipelined asbegin  for z in (select t1.division_id,                   t1.dt          dt1,                   t2.dt          dt2,                   t1.sales_amt   sales_amt1,                   t2.sales_amt   sales_amt2              from (select dt, division_id, sales_amt                      from division_sales                     where dt = in_dt1) t1,                   (select dt, division_id, sales_amt                      from division_sales                     where dt = in_dt2) t2             where t1.division_id = t2.division_id) loop    pipe row(t_division_sales_report(z.division_id,                                     z.dt1,                                     z.dt2,                                     z.sales_amt1,                                     z.sales_amt2));  end loop;end;

Обращаться к ней можно так:

select *  from table(func_division_sales(to_date('30.04.2020', 'dd.mm.yyyy'),                                 to_date('11.09.2020', 'dd.mm.yyyy')));

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

create or replace view complex_view as select field1, ...   from (select field1, ...           from (select field1, ... from deep_table), table1          where ...),        table2  where ...;

И запрос из вьюшки с фиксированным значением field1 может иметь плохой план выполнения.

select field1, ... from complex_view where field1 = 'myvalue';

Т.е. вместо того, чтобы сначала отфильтровать deep_table по условию field1 = 'myvalue', запрос может сначала соединить все таблицы, обработав излишне большой объём данных, а потом уже фильтровать результат по условию field1 = 'myvalue'. Такой сложности можно избежать, если сделать вместо вьюшки pipelined функцию с параметром, значение которого присваивается полю field1.

Использование динамической статистики в запросах


Бывает, что один и тот же запрос в БД Oracle обрабатывает всякий раз различный объём данных в использующихся в нём таблицах и подзапросах. Как заставить оптимизатор всякий раз понимать, какой из способов соединения таблиц на этот раз лучше и какие индексы использовать? Рассмотрим, например, запрос, который соединяет порцию изменившихся с последней загрузки остатков по счетам со справочником счетов. Порция изменившихся остатков по счетам сильно меняется от загрузки к загрузке, составляя то сотни строк, то миллионы строк. В зависимости от размера этой порции требуется соединять изменившиеся остатки со счетами то способом /*+ use_nl*/, то способом /*+ use_hash*/. Всякий раз повторно собирать статистику неудобно, особенно, если от загрузки к загрузке изменяется количество строк не в соединяемой таблице, а в соединяемом подзапросе. На помощь тут может прийти хинт /*+ dynamic_sampling()*/. Покажем, как он влияет, на примере запроса. Пусть таблица change_balances содержит изменения остатков, а accounts справочник счетов. Соединяем эти таблицы по полям account_id, имеющимся в каждой из таблиц. В начале эксперимента запишем в эти таблицы побольше строк и не будем менять их содержимое.
Сначала возьмём 10% изменений остатков в таблице change_balances и посмотрим, какой план будет с использованием dynamic_sampling:

SQL> EXPLAIN PLAN  2   SET statement_id = 'test1'  3   INTO plan_table  4  FOR  with c as  5   (select /*+ dynamic_sampling(change_balances 2)*/  6     account_id, balance_amount  7      from change_balances  8     where mod(account_id, 10) = 0)  9  select a.account_id, a.account_number, c.balance_amount 10    from c, accounts a 11   where c.account_id = a.account_id;Explained.SQL>SQL> SELECT * FROM table (DBMS_XPLAN.DISPLAY);Plan hash value: 874320301----------------------------------------------------------------------------------------------| Id  | Operation          | Name            | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |----------------------------------------------------------------------------------------------|   0 | SELECT STATEMENT   |                 |  9951K|   493M|       |   140K  (1)| 00:28:10 ||*  1 |  HASH JOIN         |                 |  9951K|   493M|  3240K|   140K  (1)| 00:28:10 ||*  2 |   TABLE ACCESS FULL| CHANGE_BALANCES |   100K|  2057K|       |  7172   (1)| 00:01:27 ||   3 |   TABLE ACCESS FULL| ACCOUNTS        |    10M|   295M|       |   113K  (1)| 00:22:37 |----------------------------------------------------------------------------------------------Predicate Information (identified by operation id):---------------------------------------------------   1 - access("ACCOUNT_ID"="A"."ACCOUNT_ID")   2 - filter(MOD("ACCOUNT_ID",10)=0)Note-----   - dynamic sampling used for this statement (level=2)20 rows selected.

Итак, видим, что предлагается пройти таблицы change_balances и accounts с помощью full scan и соединить их посредством hash join.
Теперь резко уменьшим выборку из change_balances. Возьмём 0.1% изменений остатков и посмотрим, какой план будет с использованием dynamic_sampling:

SQL> EXPLAIN PLAN  2   SET statement_id = 'test2'  3   INTO plan_table  4  FOR  with c as  5   (select /*+ dynamic_sampling(change_balances 2)*/  6     account_id, balance_amount  7      from change_balances  8     where mod(account_id, 1000) = 0)  9  select a.account_id, a.account_number, c.balance_amount 10    from c, accounts a 11   where c.account_id = a.account_id;Explained.SQL>SQL> SELECT * FROM table (DBMS_XPLAN.DISPLAY);Plan hash value: 2360715730-------------------------------------------------------------------------------------------------------| Id  | Operation                    | Name                   | Rows  | Bytes | Cost (%CPU)| Time     |-------------------------------------------------------------------------------------------------------|   0 | SELECT STATEMENT             |                        | 73714 |  3743K| 16452   (1)| 00:03:18 ||   1 |  NESTED LOOPS                |                        |       |       |            |          ||   2 |   NESTED LOOPS               |                        | 73714 |  3743K| 16452   (1)| 00:03:18 ||*  3 |    TABLE ACCESS FULL         | CHANGE_BALANCES        |   743 | 15603 |  7172   (1)| 00:01:27 ||*  4 |    INDEX RANGE SCAN          | IX_ACCOUNTS_ACCOUNT_ID |   104 |       |     2   (0)| 00:00:01 ||   5 |   TABLE ACCESS BY INDEX ROWID| ACCOUNTS               |    99 |  3069 |   106   (0)| 00:00:02 |-------------------------------------------------------------------------------------------------------Predicate Information (identified by operation id):---------------------------------------------------   3 - filter(MOD("ACCOUNT_ID",1000)=0)   4 - access("ACCOUNT_ID"="A"."ACCOUNT_ID")Note-----   - dynamic sampling used for this statement (level=2)22 rows selected.

На этот раз к таблице change_balances таблица accounts присоединяется посредством nested loops и используется индекс для чтения строк из accounts.
Если же хинт dynamic_sampling убрать, то во втором случае план останется такой же, как в первом случае, и это не оптимально.
Подробности о хинте dynamic_sampling и возможных значениях его числового аргумента можно найти в документации.

Как сохранить план запроса при вставке данных через database link


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

insert into dwh_table  (field1, field2)  select field1, field2 from vw_for_dwh_table@xe_link;

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

SQL> EXPLAIN PLAN  2   SET statement_id = 'test'  3   INTO plan_table  4  FOR  insert into dwh_table  5    (field1, field2)  6    select field1, field2 from vw_for_dwh_table@xe_link;Explained.SQL>SQL> SELECT * FROM table (DBMS_XPLAN.DISPLAY);Plan hash value: 1788691278-------------------------------------------------------------------------------------------------------------| Id  | Operation                | Name             | Rows  | Bytes | Cost (%CPU)| Time     | Inst   |IN-OUT|-------------------------------------------------------------------------------------------------------------|   0 | INSERT STATEMENT         |                  |     1 |  2015 |     2   (0)| 00:00:01 |        |      ||   1 |  LOAD TABLE CONVENTIONAL | DWH_TABLE        |       |       |            |          |        |      ||   2 |   REMOTE                 | VW_FOR_DWH_TABLE |     1 |  2015 |     2   (0)| 00:00:01 | XE_LI~ | R->S |-------------------------------------------------------------------------------------------------------------Remote SQL Information (identified by operation id):----------------------------------------------------   2 - SELECT /*+ OPAQUE_TRANSFORM */ "FIELD1","FIELD2" FROM "VW_FOR_DWH_TABLE" "VW_FOR_DWH_TABLE"       (accessing 'XE_LINK' )16 rows selected.

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

declare  cursor cr is    select field1, field2 from vw_for_dwh_table@xe_link;  cr_row cr%rowtype;begin  open cr;  loop    fetch cr      into cr_row;    insert into dwh_table      (field1, field2)    values      (cr_row.field1, cr_row.field2);    exit when cr%notfound;  end loop;  close cr;end;

Запрос из курсора

select field1, field2 from vw_for_dwh_table@xe_link;

в отличие от вставки

insert into dwh_table  (field1, field2)  select field1, field2 from vw_for_dwh_table@xe_link;

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

Запуск процедур в параллельных сессиях


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

create table PARALLEL_PROC_GROUP_LIST(  group_id   INTEGER,  group_name VARCHAR2(4000));comment on column PARALLEL_PROC_GROUP_LIST.group_id  is 'Номер группы параллельно запускаемых процедур';comment on column PARALLEL_PROC_GROUP_LIST.group_name  is 'Название группы параллельно запускаемых процедур';

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

create table PARALLEL_PROC_LIST(  group_id    INTEGER,  proc_script VARCHAR2(4000),  is_active   CHAR(1) default 'Y');comment on column PARALLEL_PROC_LIST.group_id  is 'Номер группы параллельно запускаемых процедур';comment on column PARALLEL_PROC_LIST.proc_script  is 'Pl/sql блок с кодом процедуры';comment on column PARALLEL_PROC_LIST.is_active  is 'Y - active, N - inactive. С помощью этого поля можно временно отключать процедуру из группы';

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

create table PARALLEL_PROC_LOG(  run_id      INTEGER,  group_id    INTEGER,  proc_script VARCHAR2(4000),  job_id      INTEGER,  start_time  DATE,  end_time    DATE);comment on column PARALLEL_PROC_LOG.run_id  is 'Номер запуска процедуры run_in_parallel';comment on column PARALLEL_PROC_LOG.group_id  is 'Номер группы параллельно запускаемых процедур';comment on column PARALLEL_PROC_LOG.proc_script  is 'Pl/sql блок с кодом процедуры';comment on column PARALLEL_PROC_LOG.job_id  is 'Job_id джоба, в котором была запущена эта процедура';comment on column PARALLEL_PROC_LOG.start_time  is 'Время начала работы';comment on column PARALLEL_PROC_LOG.end_time  is 'Время окончания работы';create sequence Seq_Parallel_Proc_Log;

Теперь приведём код процедуры по запуску параллельных потоков:

create or replace procedure run_in_parallel(in_group_id integer) as  -- Процедура по параллельному запуску процедур из таблицы parallel_proc_list.  -- Параметр - номер группы из parallel_proc_list  v_run_id             integer;  v_job_id             integer;  v_job_id_list        varchar2(32767);  v_job_id_list_ext    varchar2(32767);  v_running_jobs_count integer;begin  select seq_parallel_proc_log.nextval into v_run_id from dual;  -- submit jobs with the same parallel_proc_list.in_group_id  -- store seperated with ',' JOB_IDs in v_job_id_list  v_job_id_list     := null;  v_job_id_list_ext := null;  for z in (select pt.proc_script              from parallel_proc_list pt             where pt.group_id = in_group_id               and pt.is_active = 'Y') loop    dbms_job.submit(v_job_id, z.proc_script);    insert into parallel_proc_log      (run_id, group_id, proc_script, job_id, start_time, end_time)    values      (v_run_id, in_group_id, z.proc_script, v_job_id, sysdate, null);    v_job_id_list     := v_job_id_list || ',' || to_char(v_job_id);    v_job_id_list_ext := v_job_id_list_ext || ' union all select ' ||                         to_char(v_job_id) || ' job_id from dual';  end loop;  commit;  v_job_id_list     := substr(v_job_id_list, 2);  v_job_id_list_ext := substr(v_job_id_list_ext, 12);  -- loop while not all jobs finished  loop    -- set parallel_proc_log.end_time for finished jobs    execute immediate 'update parallel_proc_log set end_time = sysdate where job_id in (' ||                      v_job_id_list_ext ||                      ' minus select job from user_jobs where job in (' ||                      v_job_id_list ||                      ') minus select job_id from parallel_proc_log where job_id in (' ||                      v_job_id_list || ') and end_time is not null)';    commit;    -- check whether all jobs finished    execute immediate 'select count(1) from user_jobs where job in (' ||                      v_job_id_list || ')'      into v_running_jobs_count;    -- if all jobs finished then exit    exit when v_running_jobs_count = 0;    -- sleep a little    sys.dbms_lock.sleep(0.1);  end loop;end;

Проверим, как работает процедура run_in_parallel. Создадим тестовую процедуру, которую будем вызывать в параллельных сессиях.

create or replace procedure sleep(in_seconds integer) asbegin  sys.Dbms_Lock.Sleep(in_seconds);end;

Заполним название группы и таблицу со cкриптами, которые будут выполняться параллельно.

insert into PARALLEL_PROC_GROUP_LIST(group_id, group_name) values(1, 'Тестовая группа');insert into PARALLEL_PROC_LIST(group_id, proc_script, is_active) values(1, 'begin sleep(5); end;', 'Y');insert into PARALLEL_PROC_LIST(group_id, proc_script, is_active) values(1, 'begin sleep(10); end;', 'Y');

Запустим группу параллельных процедур.

begin  run_in_parallel(1);end;

По завершении посмотрим лог.

select * from PARALLEL_PROC_LOG;

RUN_ID GROUP_ID PROC_SCRIPT JOB_ID START_TIME END_TIME
1 1 begin sleep(5); end; 1 11.09.2020 15:00:51 11.09.2020 15:00:56
1 1 begin sleep(10); end; 2 11.09.2020 15:00:51 11.09.2020 15:01:01

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

Протягивание остатков


Опишем вариант решения достаточно типовой банковской задачи по протягиванию остатков. Допустим, имеется таблица фактов изменения остатков по счетам. Требуется на каждый день календаря указать актуальный остаток по счёту (последний за день). Такая информация часто бывает нужна в хранилищах данных. Если в какой-то день не было движений по счёту, то нужно повторить последний известный остаток. Если объёмы данных и вычислительные мощности сервера позволяют, то можно решить такую задачу с помощью SQL-запроса, даже не прибегая к PL/SQL. Поможет нам в этом функция last_value(* ignore nulls) over(partition by * order by *), которая протянет последний известный остаток на последующие даты, в которых не было изменений.
Создадим таблицу и заполним её тестовыми данными.

create table ACCOUNT_BALANCE(  dt           DATE,  account_id   INTEGER,  balance_amt  NUMBER,  turnover_amt NUMBER);comment on column ACCOUNT_BALANCE.dt  is 'Дата и время остатка по счёту';comment on column ACCOUNT_BALANCE.account_id  is 'Номер счёта';comment on column ACCOUNT_BALANCE.balance_amt  is 'Остаток по счёту';comment on column ACCOUNT_BALANCE.turnover_amt  is 'Оборот по счёту';insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('01.01.2020 00:00:00','dd.mm.yyyy hh24.mi.ss'), 1, 23, 23);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('05.01.2020 01:00:00','dd.mm.yyyy hh24.mi.ss'), 1, 45, 22);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('05.01.2020 20:00:00','dd.mm.yyyy hh24.mi.ss'), 1, 44, -1);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('05.01.2020 00:00:00','dd.mm.yyyy hh24.mi.ss'), 2, 67, 67);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('05.01.2020 20:00:00','dd.mm.yyyy hh24.mi.ss'), 2, 77, 10);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('07.01.2020 00:00:00','dd.mm.yyyy hh24.mi.ss'), 2, 72, -5);

Нижеприведённый запрос решает нашу задачу. Подзапрос cld содержит календарь дат, в подзапросе ab группируем остатки за каждый день, в подзапросе a запоминаем перечень всех счетов и дату начала истории по каждому счёту, в подзапросе pre для каждого счёта составляем календарь дней с начала его истории. Финальный запрос присоединяет к календарю дней активности каждого счёта последние остатки на каждый день и протягивает их на дни, в которых не было изменений.

with cld as (select /*+ materialize*/   to_date('01.01.2020', 'dd.mm.yyyy') + level - 1 dt    from dual  connect by level <= 10),ab as (select trunc(dt) dt,         account_id,         max(balance_amt) keep(dense_rank last order by dt) balance_amt,         sum(turnover_amt) turnover_amt    from account_balance   group by trunc(dt), account_id),a as (select min(dt) min_dt, account_id from ab group by account_id),pre as (select cld.dt, a.account_id from cld left join a on cld.dt >= a.min_dt)select pre.dt,       pre.account_id,       last_value(ab.balance_amt ignore nulls) over(partition by pre.account_id order by pre.dt) balance_amt,       nvl(ab.turnover_amt, 0) turnover_amt  from pre  left join ab    on pre.dt = ab.dt   and pre.account_id = ab.account_id order by 2, 1;

Результат запроса соответствует ожиданиям.
DT ACCOUNT_ID BALANCE_AMT TURNOVER_AMT
01.01.2020 1 23 23
02.01.2020 1 23 0
03.01.2020 1 23 0
04.01.2020 1 23 0
05.01.2020 1 44 21
06.01.2020 1 44 0
07.01.2020 1 44 0
08.01.2020 1 44 0
09.01.2020 1 44 0
10.01.2020 1 44 0
05.01.2020 2 77 77
06.01.2020 2 77 0
07.01.2020 2 72 -5
08.01.2020 2 72 0
09.01.2020 2 72 0
10.01.2020 2 72 0

Объединение нескольких историй в одну


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

create table HIST1(  primary_key_id INTEGER,  start_dt       DATE,  attribute1     NUMBER);insert into HIST1(primary_key_id, start_dt, attribute1) values(1, to_date('2014-01-01','yyyy-mm-dd'), 7);insert into HIST1(primary_key_id, start_dt, attribute1) values(1, to_date('2015-01-01','yyyy-mm-dd'), 8);insert into HIST1(primary_key_id, start_dt, attribute1) values(1, to_date('2016-01-01','yyyy-mm-dd'), 9);insert into HIST1(primary_key_id, start_dt, attribute1) values(2, to_date('2014-01-01','yyyy-mm-dd'), 17);insert into HIST1(primary_key_id, start_dt, attribute1) values(2, to_date('2015-01-01','yyyy-mm-dd'), 18);insert into HIST1(primary_key_id, start_dt, attribute1) values(2, to_date('2016-01-01','yyyy-mm-dd'), 19);create table HIST2(  primary_key_id INTEGER,  start_dt       DATE,  attribute2     NUMBER); insert into HIST2(primary_key_id, start_dt, attribute2) values(1, to_date('2015-01-01','yyyy-mm-dd'), 4);insert into HIST2(primary_key_id, start_dt, attribute2) values(1, to_date('2016-01-01','yyyy-mm-dd'), 5);insert into HIST2(primary_key_id, start_dt, attribute2) values(1, to_date('2017-01-01','yyyy-mm-dd'), 6);insert into HIST2(primary_key_id, start_dt, attribute2) values(2, to_date('2015-01-01','yyyy-mm-dd'), 14);insert into HIST2(primary_key_id, start_dt, attribute2) values(2, to_date('2016-01-01','yyyy-mm-dd'), 15);insert into HIST2(primary_key_id, start_dt, attribute2) values(2, to_date('2017-01-01','yyyy-mm-dd'), 16);create table HIST3(  primary_key_id INTEGER,  start_dt       DATE,  attribute3     NUMBER); insert into HIST3(primary_key_id, start_dt, attribute3) values(1, to_date('2016-01-01','yyyy-mm-dd'), 10);insert into HIST3(primary_key_id, start_dt, attribute3) values(1, to_date('2017-01-01','yyyy-mm-dd'), 20);insert into HIST3(primary_key_id, start_dt, attribute3) values(1, to_date('2018-01-01','yyyy-mm-dd'), 30);insert into HIST3(primary_key_id, start_dt, attribute3) values(2, to_date('2016-01-01','yyyy-mm-dd'), 110);insert into HIST3(primary_key_id, start_dt, attribute3) values(2, to_date('2017-01-01','yyyy-mm-dd'), 120);insert into HIST3(primary_key_id, start_dt, attribute3) values(2, to_date('2018-01-01','yyyy-mm-dd'), 130);

Целью является загрузка единой истории изменения трёх атрибутов в одну таблицу.
Ниже приведён запрос, решающий такую задачу. В нём сначала формируется диагональная таблица q1 с данными из разных источников по разным атрибутам (отсутствующие в источнике атрибуты заполняются null-ами). Затем с помощью функции last_value(* ignore nulls) диагональная таблица схлопывается в единую историю, а последние известные значения атрибутов протягиваются вперёд на те даты, в которые изменений по ним не было:

select primary_key_id,       start_dt,       nvl(lead(start_dt - 1)           over(partition by primary_key_id order by start_dt),           to_date('9999-12-31', 'yyyy-mm-dd')) as end_dt,       last_value(attribute1 ignore nulls) over(partition by primary_key_id order by start_dt) as attribute1,       last_value(attribute2 ignore nulls) over(partition by primary_key_id order by start_dt) as attribute2,       last_value(attribute3 ignore nulls) over(partition by primary_key_id order by start_dt) as attribute3  from (select primary_key_id,               start_dt,               max(attribute1) as attribute1,               max(attribute2) as attribute2,               max(attribute3) as attribute3          from (select primary_key_id,                       start_dt,                       attribute1,                       cast(null as number) attribute2,                       cast(null as number) attribute3                  from hist1                union all                select primary_key_id,                       start_dt,                       cast(null as number) attribute1,                       attribute2,                       cast(null as number) attribute3                  from hist2                union all                select primary_key_id,                       start_dt,                       cast(null as number) attribute1,                       cast(null as number) attribute2,                       attribute3                  from hist3) q1         group by primary_key_id, start_dt) q2 order by primary_key_id, start_dt;

Результат получается такой:
PRIMARY_KEY_ID START_DT END_DT ATTRIBUTE1 ATTRIBUTE2 ATTRIBUTE3
1 01.01.2014 31.12.2014 7 NULL NULL
1 01.01.2015 31.12.2015 8 4 NULL
1 01.01.2016 31.12.2016 9 5 10
1 01.01.2017 31.12.2017 9 6 20
1 01.01.2018 31.12.9999 9 6 30
2 01.01.2014 31.12.2014 17 NULL NULL
2 01.01.2015 31.12.2015 18 14 NULL
2 01.01.2016 31.12.2016 19 15 110
2 01.01.2017 31.12.2017 19 16 120
2 01.01.2018 31.12.9999 19 16 130

Нормалайзер


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

create table DENORMALIZED_TABLE(  id  INTEGER,  val VARCHAR2(4000));insert into DENORMALIZED_TABLE(id, val) values(1, 'aaa,cccc,bb');insert into DENORMALIZED_TABLE(id, val) values(2, 'ddd');insert into DENORMALIZED_TABLE(id, val) values(3, 'fffff,e');

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

select id, regexp_substr(val, '[^,]+', 1, column_value) val, column_value  from denormalized_table,       table(cast(multiset                  (select level                     from dual                   connect by regexp_instr(val, '[^,]+', 1, level) > 0) as                  sys.odcinumberlist)) order by id, column_value;

Результат получается такой:
ID VAL COLUMN_VALUE
1 aaa 1
1 cccc 2
1 bb 3
2 ddd 1
3 fffff 1
3 e 2

Визуализация в формате SVG


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

create table graph_data(dt date, val number, radius number);insert into graph_data(dt, val, radius) values (to_date('01.01.2020','dd.mm.yyyy'), 12, 3);insert into graph_data(dt, val, radius) values (to_date('02.01.2020','dd.mm.yyyy'), 15, 4);insert into graph_data(dt, val, radius) values (to_date('05.01.2020','dd.mm.yyyy'), 17, 5);insert into graph_data(dt, val, radius) values (to_date('06.01.2020','dd.mm.yyyy'), 13, 6);insert into graph_data(dt, val, radius) values (to_date('08.01.2020','dd.mm.yyyy'),  3, 7);insert into graph_data(dt, val, radius) values (to_date('10.01.2020','dd.mm.yyyy'), 20, 8);insert into graph_data(dt, val, radius) values (to_date('11.01.2020','dd.mm.yyyy'), 18, 9);

dt это дата актуальности,
val это числовой показатель, динамику которого по времени мы визуализируем,
radius это ещё один числовой показатель, который будем рисовать в виде кружка с таким радиусом.
Скажем пару слов о формате SVG. Это формат векторной графики, который можно смотреть в современных браузерах и конвертировать в другие графические форматы. В нём, среди прочего, можно рисовать линии, кружки и писать текст:

<line x1="94" x2="94" y1="15" y2="675" style="stroke:rgb(150,255,255); stroke-width:1px"/><circle cx="30" cy="279" r="3" style="fill:rgb(255,0,0)"/><text x="7" y="688" font-size="10" fill="rgb(0,150,255)">2020-01-01</text>

Ниже SQL-запрос к Oracle, который строит график из данных в этой таблице. Здесь подзапрос const содержит различные константные настройки размеры картинки, количество меток на осях графика, цвета линий и кружочков, размеры шрифта и т.д. В подзапросе gd1 мы приводим данные из таблицы graph_data к координатам x и y на рисунке. Подзапрос gd2 запоминает предыдущие по времени точки, из которых нужно вести линии к новым точкам. Блок header это заголовок картинки с белым фоном. Блок vertical lines рисует вертикальные линии. Блок dates under vertical lines подписывает даты на оси x. Блок horizontal lines рисует горизонтальные линии. Блок values near horizontal lines подписывает значения на оси y. Блок circles рисует кружочки указанного в таблице graph_data радиуса. Блок graph data строит из линий график динамики показателя val из таблицы graph_data. Блок footer добавляет замыкающий тэг.

with const as (select 700 viewbox_width,         700 viewbox_height,         30 left_margin,         30 right_margin,         15 top_margin,         25 bottom_margin,         max(dt) - min(dt) + 1 num_vertical_lines,         11 num_horizontal_lines,         'rgb(150,255,255)' stroke_vertical_lines,         '1px' stroke_width_vertical_lines,         10 font_size_dates,         'rgb(0,150,255)' fill_dates,         23 x_dates_pad,         13 y_dates_pad,         'rgb(150,255,255)' stroke_horizontal_lines,         '1px' stroke_width_horizontal_lines,         10 font_size_values,         'rgb(0,150,255)' fill_values,         4 x_values_pad,         2 y_values_pad,         'rgb(255,0,0)' fill_circles,         'rgb(51,102,0)' stroke_graph,         '1px' stroke_width_graph,         min(dt) min_dt,         max(dt) max_dt,         max(val) max_val    from graph_data),gd1 as (select graph_data.dt,         const.left_margin +         (const.viewbox_width - const.left_margin - const.right_margin) *         (graph_data.dt - const.min_dt) / (const.max_dt - const.min_dt) x,         const.viewbox_height - const.bottom_margin -         (const.viewbox_height - const.top_margin - const.bottom_margin) *         graph_data.val / const.max_val y,         graph_data.radius    from graph_data, const),gd2 as (select dt,         round(nvl(lag(x) over(order by dt), x)) prev_x,         round(x) x,         round(nvl(lag(y) over(order by dt), y)) prev_y,         round(y) y,         radius    from gd1)/* header */select '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' txt  from dualunion allselect '<svg version="1.1" width="' || viewbox_width || '" height="' ||       viewbox_height || '" viewBox="0 0 ' || viewbox_width || ' ' ||       viewbox_height ||       '" style="background:yellow" baseProfile="full" xmlns="http://personeltest.ru/away/www.w3.org/2000/svg" xmlns:xlink="http://personeltest.ru/away/www.w3.org/1999/xlink" xmlns:ev="http://personeltest.ru/away/www.w3.org/2001/xml-events">'  from constunion allselect '<title>Test graph</title>'  from dualunion allselect '<desc>Test graph</desc>'  from dualunion allselect '<rect width="' || viewbox_width || '" height="' || viewbox_height ||       '" style="fill:white" />'  from constunion all/* vertical lines */select '<line x1="' ||       to_char(round(left_margin +                     (viewbox_width - left_margin - right_margin) *                     (level - 1) / (num_vertical_lines - 1))) || '" x2="' ||       to_char(round(left_margin +                     (viewbox_width - left_margin - right_margin) *                     (level - 1) / (num_vertical_lines - 1))) || '" y1="' ||       to_char(round(top_margin)) || '" y2="' ||       to_char(round(viewbox_height - bottom_margin)) || '" style="stroke:' ||       const.stroke_vertical_lines || '; stroke-width:' ||       const.stroke_width_vertical_lines || '"/>'  from constconnect by level <= num_vertical_linesunion all/* dates under vertical lines */select '<text x="' ||       to_char(round(left_margin +                     (viewbox_width - left_margin - right_margin) *                     (level - 1) / (num_vertical_lines - 1) - x_dates_pad)) ||       '" y="' ||       to_char(round(viewbox_height - bottom_margin + y_dates_pad)) ||       '" font-size="' || font_size_dates || '" fill="' || fill_dates || '">' ||       to_char(min_dt + level - 1, 'yyyy-mm-dd') || '</text>'  from constconnect by level <= num_vertical_linesunion all/* horizontal lines */select '<line x1="' || to_char(round(left_margin)) || '" x2="' ||       to_char(round(viewbox_width - right_margin)) || '" y1="' ||       to_char(round(top_margin +                     (viewbox_height - top_margin - bottom_margin) *                     (level - 1) / (num_horizontal_lines - 1))) || '" y2="' ||       to_char(round(top_margin +                     (viewbox_height - top_margin - bottom_margin) *                     (level - 1) / (num_horizontal_lines - 1))) ||       '" style="stroke:' || const.stroke_horizontal_lines ||       '; stroke-width:' || const.stroke_width_horizontal_lines || '"/>'  from constconnect by level <= num_horizontal_linesunion all/* values near horizontal lines */select '<text text-anchor="end" x="' ||       to_char(round(left_margin - x_values_pad)) || '" y="' ||       to_char(round(viewbox_height - bottom_margin -                     (viewbox_height - top_margin - bottom_margin) *                     (level - 1) / (num_horizontal_lines - 1) +                     y_values_pad)) || '" font-size="' || font_size_values ||       '" fill="' || fill_values || '">' ||       to_char(round(max_val / (num_horizontal_lines - 1) * (level - 1), 2)) ||       '</text>'  from constconnect by level <= num_horizontal_linesunion all/* circles */select '<circle cx="' || to_char(gd2.x) || '" cy="' || to_char(gd2.y) ||       '" r="' || gd2.radius || '" style="fill:' || const.fill_circles ||       '"/>'  from gd2, constunion all/* graph data */select '<line x1="' || to_char(gd2.prev_x) || '" x2="' || to_char(gd2.x) ||       '" y1="' || to_char(gd2.prev_y) || '" y2="' || to_char(gd2.y) ||       '" style="stroke:' || const.stroke_graph || '; stroke-width:' ||       const.stroke_width_graph || '"/>'  from gd2, constunion all/* footer */select '</svg>' from dual;

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



Приложение поиска по метаданным Oracle


Представьте себе, что стоит задача найти что-либо в исходном коде на Oracle, поискав информацию сразу на нескольких серверах. Речь идёт о поиске по объектам словаря данных Oracle. Рабочим местом для поиска является веб-интерфейс, куда пользователь-программист вводит искомую строку и выбирает галочками, на каких серверах Oracle осуществить этот поиск.
Веб-поисковик умеет искать строку по серверным объектам Oracle одновременно в нескольких различных базах данных банка. Например, можно поискать:
  • Где в коде Oracle зашита константа 61209, представляющая собой номер счёта второго порядка?
  • Где в коде и на каких серверах используется таблица accounts (в т.ч. через database link)?
  • С какого сервера из какой хранимой процедуры или триггера приходит сгенерированная программистом ошибка, например, ORA-20001 Курс валюты не найден?
  • Прописан ли планируемый к удалению индекс IX_CLIENTID где-либо явным образом в хинтах оптимизатора в SQL-запросах?
  • Используются ли где-либо (в т.ч. через database link) планируемые к удалению таблица, поле, процедура, функция и т.д.?
  • Где в коде явно зашит чей-то е-мэйл или номер телефона? Такие вещи лучше выносить из серверных объектов в настроечные таблицы.
  • Где в коде на серверах используется зависящий от версии Oracle функционал? Например, функция wm_concat выдаёт различный тип данных на выходе в зависимости от версии Oracle. Это может быть критично и требует внимания при миграции на более новую версию.
  • Где в коде используется какой-либо редкий приём, на который программисту хочется посмотреть, как на образец? Например, поискать в коде Oracle примеры использования функций sys_connect_by_path, regexp_instr или хинта push_subq.

По результатам поиска пользователю выдаётся информация, на каком сервере в коде каких функций, процедур, пакетов, триггеров, вьюшек и т.п. найдены требуемые результаты.
Опишем, как реализован такой поисковик.
Клиентская часть не сложная. Веб-интерфейс получает введённую пользователем поисковую строку, список серверов для поиска и логин пользователя. Веб-страница передаёт их в хранимую процедуру Oracle на сервере-обработчике. История обращений к поисковику, т.е. кто какой запрос выполнял, на всякий случай журналируется.
Получив поисковый запрос, серверная часть на поисковом сервере Oracle запускает в параллельных джобах несколько процедур, которые по database links на выбранных серверах Oracle сканируют следующие представления словаря данных в поисках искомой строки: dba_col_comments, dba_jobs, dba_mviews, dba_objects, dba_scheduler_jobs, dba_source, dba_tab_cols, dba_tab_comments, dba_views. Каждая из процедур, если что-то обнаружила, записывает найденное в таблицу результатов поиска (с соответствующим ID поискового запроса). Когда все поисковые процедуры завершили работу, клиентская часть выдаёт пользователю всё, что записалось в таблицу результатов поиска с соответствующим ID поискового запроса.
Но это ещё не всё. Помимо поиска по словарю данных Oracle в описанный механизм прикрутили ещё и поиск по репозиторию Informatica PowerCenter. Informatica PowerCenter является популярным ETL-средством, использующимся в Сбербанке при загрузке различной информации в хранилища данных. Informatica PowerCenter имеет открытую хорошо задокументированную структуру репозитория. По этому репозиторию есть возможность искать информацию так же, как и по словарю данных Oracle. Какие таблицы и поля используются в коде загрузок, разработанном на Informatica PowerCenter? Что можно найти в трансформациях портов и явных SQL-запросах? Вся эта информация имеется в структурах репозитория и может быть найдена. Для знатоков PowerCenter напишу, что наш поисковик сканирует следующие места репозитория в поисках маппингов, сессий или воркфловов, содержащих в себе где-то искомую строку: sql override, mapplet attributes, ports, source definitions in mappings, source definitions, target definitions in mappings, target_definitions, mappings, mapplets, workflows, worklets, sessions, commands, expression ports, session instances, source definition fields, target definition fields, email tasks.

Автор: Михаил Гричик, эксперт профессионального сообщества Сбербанка SberProfi DWH/BigData.

Профессиональное сообщество SberProfi DWH/BigData отвечает за развитие компетенций в таких направлениях, как экосистема Hadoop, Teradata, Oracle DB, GreenPlum, а также BI инструментах Qlik, SAP BO, Tableau и др.
Подробнее..

Как мы, сотрудники Сбера, считаем и инвестируем свои деньги

19.10.2020 10:13:52 | Автор: admin


Нужно ли покупать автомобиль за 750 тысяч рублей при том, что вы ездите 18 раз в месяц или дешевле пользоваться такси? Если вы работаете на заднем сидении или слушаете музыку как это меняет оценку? Как правильнее покупать квартиру в какой момент оптимально заканчивать копить на депозите и делать первый взнос по ипотеке? Или даже тривиальный вопрос: выгоднее положить деньги на депозит под 6% с ежемесячной капитализацией или под 6,2% с ежегодной капитализацией? Большинство людей даже не пытается производить такие подсчёты и даже не хотят собирать детальную информацию о своих деньгах. Вместо подсчётов подключают чувства и эмоции. Либо делают какую-то узкую оценку, например, детально подсчитывают годовую стоимость владения автомобилем, в то время как все эти расходы могут составлять лишь 5% от общих трат (а траты на другие стороны жизни при этом не подсчитывают). Мозг человека подвержен когнитивным искажениям. Например, сложно бросить, несмотря на неокупаемость, дело, в которое вложены масса времени и денег. Люди обычно излишне оптимистичны и недооценивают риски, а также легко внушаемы и могут купить дорогую безделушку или вложиться в финансовую пирамиду.
Понятное дело, в случае банка эмоциональная оценка не работает. Поэтому я хочу сначала рассказать о том, как оценивает деньги обычное физлицо (я, в том числе), и как это делает банк. Ниже будет немного финансового ликбеза и много про аналитику данных в Сбербанке для всего банка в целом.
Полученные выводы приведены только в качестве примера и не могут расцениваться как рекомендации для частных инвесторов, поскольку не учитывают множества факторов, оставшихся за рамками данной статьи.
Например, любое событие типа черный лебедь в макроэкономике, в корпоративном управлении любой из компаний и пр., может привести к кардинальным изменениям.

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

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

Часто люди принимают финансовые решения, не располагая полной информацией о динамике своих собственных доходов и расходов, не имея оценки стоимости собственного имущества, не учитывая в расчётах инфляцию и т.п.
Иногда люди допускают ошибки, например, берут кредит, думая, что смогут его выплачивать, а затем не справляются. При этом ответ на вопрос, осилит ли человек обслуживание кредита, зачастую известен заранее. Нужно просто знать, сколько зарабатываешь, сколько тратишь, какова динамика изменений этих показателей.
Или, например, человек получает какую-то зарплату на работе, её периодически повышают, преподнося, как оценку заслуг. А в действительности по сравнению с инфляцией заработок этого человека может падать, и он может этого не осознавать, если не ведёт учёт доходов.
Некоторые люди не могут оценить, какой выбор выгоднее в сложившейся у них ситуации: снимать квартиру или взять ипотеку под такую-то ставку.
И вместо того, чтобы произвести подсчёт, каковы будут расходы в том и ином случае, как-то монетизировав в расчётах нефинансовые показатели (выгоду от московской прописки оцениваю в M рублей в месяц, удобство от проживания в снимаемой возле работы квартире оцениваю в N рублей в месяц), люди бегут в интернет дискутировать с собеседниками, у которых может быть иная финансовая ситуация и другие приоритеты в оценке нефинансовых показателей.
Я за ответственное финансовое планирование. Прежде всего, предлагается осуществлять сбор следующих данных о собственном финансовом положении:

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

Учёт и оценка всего имеющегося в наличии имущества


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

Оцените, что у вас есть:

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

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



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

Учёт доходов, расходов и динамики накопления имущества


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



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

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

Эти данные позволят нам сравнить доходность и риски от вложений в сдаваемую в аренду недвижимость, в банковские депозиты и в рынок акций. При этом не забудем учесть инфляцию.
Сразу скажу, что в этом посте мы занимаемся исключительно анализом данных и не прибегаем к использованию каких-либо экономических теорий. Просто посмотрим, что говорят наши данные какой способ сохранить и преумножить сбережения в России за последние годы дал наилучший результат.
Кратко расскажем, о том, как собираются и анализируются данные, использующиеся в этой статье, и прочие данные в Сбербанке. Имеется слой реплик источников, которые хранятся в формате parquet на hadoop. Используются как внутренние источники (различные АС банка), так и внешние. Реплики источников собираются разными способами. Есть продукт stork, в основе которого лежит spark, набирает обороты и второй продукт Ab Initio AIR. Реплики источников загружаются на различные кластеры hadoop под управлением Cloudera, в том числе могут быть прилинкованы с одного кластера на другой. Кластеры разделены преимущественно по бизнес-блокам, имеются также и кластеры Лаборатории данных. На базе реплик источников строятся различные витрины данных, доступные бизнес-пользователям и data scientist-ам. Для написания этой статьи были использованы различные приложения spark, запросы к hive, приложения по анализу данных и визуализации результатов в формате графики SVG.

Исторический анализ рынка недвижимости


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

График цен в рублях без учёта инфляции:



График цен в рублях с учётом инфляции (в современных ценах):



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

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



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



Корреляционный анализ показывает, что зависимость между стоимостью аренды квартиры и стоимостью её покупки близка к линейной.
Получилось такое соотношение между стоимостью годовой аренды квартиры и стоимостью приобретения квартиры (не забудем, что годовая стоимость это 12 месячных):
Количество комнат: Отношение стоимости годовой аренды квартиры к стоимости приобретения квартиры:
1-комнатные 5,11%
2-комнатные 4,80%
3-комнатные 4,94%
Всего 4,93%

Получили среднюю оценку в 4,93% годовых доходности от сдачи квартиры в аренду сверх инфляции. Также интересен момент, что дешёвые 1-комнатные квартиры сдавать в аренду немного выгоднее. Мы сравнивали цену предложения, которая в обоих случаях (аренды и покупки) немного завышена, поэтому корректировка не требуется. Однако требуются другие корректировки: сдаваемые в аренду квартиры нужно иногда хотя бы косметически ремонтировать, некоторое время занимает поиск арендатора и квартиры пустуют, иногда в цену аренды не заложены коммунальные платежи частично или полностью, также имеет место крайне незначительное обесценивание квартир с годами.
С учётом корректировок, от сдачи жилой недвижимости в аренду можно иметь доход до 4,5% годовых (сверх того, что сама недвижимость не обесценивается). Если такая доходность впечатляет, у Сбербанка есть множество предложений на ДомКлик.

Исторический анализ ставок по депозитам


Рублёвые депозиты в России в последние несколько лет в основном обыгрывают инфляцию. Но не на 4,5%, как сдаваемая недвижимость, а, в среднем, на 2%.
На графике ниже видим динамику сравнения ставок по депозитам и уровня инфляции.



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

  • Можно фиксировать ставку по пополняемым вкладам в благоприятное время на несколько месяцев вперёд
  • Ежемесячная капитализация, свойственная многим учтённым в этих усреднённых данных вкладам, за счёт сложных процентов добавляет прибыли
  • Выше были учтены ставки по топ-10 банкам по информации от Банка России, вне топ-10 можно найти ставки несколько выше

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

Исторический анализ рынка акций


Теперь посмотрим на более разнообразный и рискованный рынок российских акций. Доходность от вложения в акции не фиксирована и может сильно меняться. Однако, если диверсифицировать активы и заниматься инвестированием длительный период, то можно проследить среднюю годовую процентную ставку, характеризующую успешность инвестирования в портфель акций.
Для далёких от темы читателей скажу пару слов об индексах акций. В России есть индекс Мосбиржи, который показывает, динамику рублевой стоимости портфеля, состоящего из 50 крупнейших российских акций. Состав индекса и доля акций каждой компании зависит от объема торговых операций, объема бизнеса, количества находящихся в обращении акций. График ниже показывает, как рос индекс Мосбиржи (т.е. такой усреднённый портфель) в последние годы.



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

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



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

Посмотрим, какой же был коэффициент роста индекса MCFTR за последние 1,2,3,,11 лет. Т.е. какова же была бы наша доходность, если бы мы купили акции в пропорциях этого индекса и регулярно реинвестировали бы полученные дивиденды в те же самые акции:
Лет Начало Конец MCFTR
нач. с
учётом
инфл.
MCFTR
кон. с
учётом
инфл.
Коэфф.
роста
Годовой
коэфф.
роста
1 30.07.2019 30.07.2020 4697,47 5095,54 1,084741 1,084741
2 30.07.2018 30.07.2020 3835,52 5095,54 1,328513 1,152612
3 30.07.2017 30.07.2020 3113,38 5095,54 1,636659 1,178472
4 30.07.2016 30.07.2020 3115,30 5095,54 1,635650 1,130896
5 30.07.2015 30.07.2020 2682,35 5095,54 1,899655 1,136933
6 30.07.2014 30.07.2020 2488,07 5095,54 2,047989 1,126907
7 30.07.2013 30.07.2020 2497,47 5095,54 2,040281 1,107239
8 30.07.2012 30.07.2020 2634,99 5095,54 1,933799 1,085929
9 30.07.2011 30.07.2020 3245,76 5095,54 1,569907 1,051390
10 30.07.2010 30.07.2020 2847,81 5095,54 1,789284 1,059907
11 30.07.2009 30.07.2020 2223,17 5095,54 2,292015 1,078318

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

Составим еще одну табличку не прибыльность за каждые последние N лет, а прибыльность за каждый из последних N одногодовых периодов:
Год Начало Конец MCFTR
нач. с
учётом
инфл.
MCFTR
кон. с
учётом
инфл.
Годовой
коэфф.
роста
1 30.07.2019 30.07.2020 4697,47 5095,54 1,084741
2 30.07.2018 30.07.2019 3835,52 4697,47 1,224728
3 30.07.2017 30.07.2018 3113,38 3835,52 1,231947
4 30.07.2016 30.07.2017 3115,30 3113,38 0,999384
5 30.07.2015 30.07.2016 2682,35 3115,30 1,161407
6 30.07.2014 30.07.2015 2488,07 2682,35 1,078085
7 30.07.2013 30.07.2014 2497,47 2488,07 0,996236
8 30.07.2012 30.07.2013 2634,99 2497,47 0,947810
9 30.07.2011 30.07.2012 3245,76 2634,99 0,811825
10 30.07.2010 30.07.2011 2847,81 3245,76 1,139739
11 30.07.2009 30.07.2010 2223,17 2847,81 1,280968

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

Теперь, для лучшего понимания, давайте абстрагируемся от этого индекса и посмотрим на примере конкретной акции, какой бы получился результат, если вложиться в эту акцию 15 лет назад, повторно вкладывать дивиденды и платить налоги. Результат посмотрим с учётом инфляции, т.е. в современных ценах. Ниже показан пример обыкновенной акции Сбербанка. Зелёный график показывает динамику стоимости портфеля, изначально состоявшего из одной акции Сбербанка в современных ценах с учётом реинвестиции дивидендов. За 15 лет инфляция обесценила рубли в 3.014135 раз. Акция Сбербанка за эти годы подорожала с 21.861 руб. до 218.15 руб., т.е. цена выросла в 9.978958 раз без учёта инфляции. За эти годы владельцу одной акции было выплачено в разное время дивидендов за вычетом налогов в сумме 40.811613 руб. Суммы выплаченных дивидендов показаны на графике красными вертикальными палочками и не относятся к самому графику, в котором дивиденды и их реинвестиция также учтены. Если всякий раз на эти дивиденды вновь покупались акции Сбербанка, то в конце периода акционер уже владел не одной, а 1.309361 акциями. С учётом реинвестиции дивидендов и инфляции исходный портфель подорожал в 4.334927 раз за 15 лет, т.е. дорожал в 1.102721 раз ежегодно. Итого, обыкновенная акция Сбербанка приносила владельцу в среднем 10,27% годовых сверх инфляции каждый из 15 последних лет:



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



Эти результаты на практике будут чуть ниже, потому что при покупке акций нужно платить небольшую брокерскую комиссию. В то же время результат может быть ещё улучшен, если использовать Индивидуальный Инвестиционный Счёт, позволяющий получить от государства налоговый вычет в некотором ограниченном объёме. Если вы не слышали о таком, предлагается поискать по аббревиатуре ИИС. Не забудем также упомянуть, что ИИС можно открыть в Сбербанке.

Итак, мы предварительно получили, что инвестировать в акции исторически выгоднее, чем в недвижимость и депозиты. Для развлечения приведём полученный в результате анализа данных хит-парад из 20 наилучших акций, которые торгуются на рынке более 10 лет. В последнем столбце видим, во сколько раз в среднем каждый год рос портфель из акций с учётом инфляции и реинвестиции дивидендов. Видим, что многие акции обыгрывали инфляцию более, чем на 10%:
Акция Начало Конец Коэфф. инфляции Нач. цена Кон. цена Рост
числа
акций
за счёт
реинве-
стиции
диви-
дендов,
раз
Итоговый
средне-
годовой
рост, раз
Лензолото 30.07.2010 30.07.2020 1,872601 1267,02 17290 2,307198 1,326066
НКНХ ап 30.07.2010 30.07.2020 1,872601 5,99 79,18 2,319298 1,322544
МГТС-4ап 30.07.2010 30.07.2020 1,872601 339,99 1980 3,188323 1,257858
Татнфт 3ап 30.07.2010 30.07.2020 1,872601 72,77 538,8 2,037894 1,232030
МГТС-5ао 30.07.2010 30.07.2020 1,872601 380,7 2275 2,487047 1,230166
Акрон 30.07.2010 30.07.2020 1,872601 809,88 5800 2,015074 1,226550
Лензол. ап 30.07.2010 30.07.2020 1,872601 845 5260 2,214068 1,220921
НКНХ ао 30.07.2010 30.07.2020 1,872601 14,117 92,45 1,896548 1,208282
Ленэнерг-п 30.07.2010 30.07.2020 1,872601 25,253 149,5 1,904568 1,196652
ГМКНорНик 30.07.2010 30.07.2020 1,872601 4970 19620 2,134809 1,162320
Сургнфгз-п 30.07.2010 30.07.2020 1,872601 13,799 37,49 2,480427 1,136619
ИРКУТ-3 30.07.2010 30.07.2020 1,872601 8,127 35,08 1,543182 1,135299
Татнфт 3ао 30.07.2010 30.07.2020 1,872601 146,94 558,4 1,612350 1,125854
Новатэк ао 30.07.2010 30.07.2020 1,872601 218,5 1080,8 1,195976 1,121908
СевСт-ао 30.07.2010 30.07.2020 1,872601 358 908,4 2,163834 1,113569
Красэсб ао 30.07.2010 30.07.2020 1,872601 3,25 7,07 2,255269 1,101105
ЧТПЗ ао 30.07.2010 30.07.2020 1,872601 55,7 209,5 1,304175 1,101088
Сбербанк-п 30.07.2010 30.07.2020 1,872601 56,85 203,33 1,368277 1,100829
ПИК ао 30.07.2010 30.07.2020 1,872601 108,26 489,5 1,079537 1,100545
ЛУКОЙЛ 30.07.2010 30.07.2020 1,872601 1720 5115 1,639864 1,100444

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

Задача. Найти акцию, стабильно дающую доход выше недвижимости (среднегодовой коэффициент роста 1.045 свыше инфляции) максимальное число раз по каждому из последних 10 одногодовых периодов, когда акция торговалась.
В этой и следующих задачах имеется в виду вышеописанная модель с реинвестицией дивидендов и учётом инфляции.
Вот победители в этой номинации согласно нашему анализу данных. Акции в верхней части таблицы из года в год стабильно показывают высокую доходность без провалов. Здесь Год 1 это 30.07.2019-30.07.2020, Год 2 это 30.07.2018-30.07.2019 и т.д.:
Акция Число
побед
над
недви-
жимо-
стью
за
после-
дние
10 лет
Год 1 Год 2 Год 3 Год 4 Год 5 Год 6 Год 7 Год 8 Год 9 Год 10
Татнфт 3ап 8 0,8573 1,4934 1,9461 1,6092 1,0470 1,1035 1,2909 1,0705 1,0039 1,2540
МГТС-4ап 8 1,1020 1,0608 1,8637 1,5106 1,7244 0,9339 1,1632 0,9216 1,0655 1,6380
ЧТПЗ ао 7 1,5532 1,2003 1,2495 1,5011 1,5453 1,2926 0,9477 0,9399 0,3081 1,3666
СевСт-ао 7 0,9532 1,1056 1,3463 1,1089 1,1955 2,0003 1,2501 0,6734 0,6637 1,3948
НКНХ ао 7 1,3285 1,5916 1,0821 0,8403 1,7407 1,3632 0,8729 0,8678 1,0716 1,7910
МГТС-5ао 7 1,1969 1,0688 1,8572 1,3789 2,0274 0,8394 1,1685 0,8364 1,0073 1,4460
Газпрнефть 7 0,8119 1,3200 1,6868 1,2051 1,1751 0,9197 1,1126 0,7484 1,1131 1,0641
Татнфт 3ао 7 0,7933 1,0807 1,9714 1,2109 1,0728 1,1725 1,0192 0,9815 1,0783 1,1785
Ленэнерг-п 7 1,3941 1,1865 1,7697 2,4403 2,2441 0,6250 1,2045 0,7784 0,4562 1,4051
НКНХ ап 7 1,3057 2,4022 1,2896 0,8209 1,2356 1,6278 0,7508 0,8449 1,5820 2,4428
Сургнфгз-п 7 1,1897 1,0456 1,2413 0,8395 0,9643 1,4957 1,2140 1,1280 1,4013 1,0031

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

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

Приведём анекдот 2008 года.
Джон Смит, выпрыгнувший из окна 75-го этажа на Уолл Стрит, после удара о землю подпрыгнул на 10 метров, чем немного отыграл свое утреннее падение.

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

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

Наша модель просчитала все варианты ширины окрестности вокруг дат выплаты дивидендов за всю историю. Были приняты следующие ограничения: M<=30, N>=20. Дело в том, что далеко не всегда ранее, чем за 30 дней до выплаты дивидендов заранее известна дата и суммы выплаты. Также дивиденды приходят на счёт далеко не сразу, а с задержкой. Считаем, что нужно от 20 дней, чтобы гарантированно получить на счёт и реинвестировать дивиденды. С такими ограничениями модель выдала следующий ответ. Наиболее оптимально покупать акции за 34 дня до даты выплаты дивидендов и продавать их через 25 дней после даты выплаты дивидендов. При таком сценарии в среднем получался рост в 3,11% за этот период, что даёт 20,9% годовых. Т.е. при рассматриваемой модели инвестирования (с реинвестицией дивидендов и учётом инфляции) если покупать акцию за 34 дня до даты выплаты дивидендов и продавать её через 25 дней после даты выплаты дивидендов, то имеем 20,9% годовых свыше уровня инфляции. Это проверено усреднением по всем случаям выплаты дивидендов из нашей базы.

Например, по привилегированной акции Сбербанка такой сценарий входа-выхода давал бы 11,72% роста свыше уровня инфляции за каждый вход-выход в окрестности даты выплаты дивидендов. Это составляет аж 98,6% годовых свыше уровня инфляции. Но это, конечно, пример случайного везения.
Акция Вход Дата выплаты дивидендов Выход Коэфф. роста
Сбербанк-п 10.05.2019 13.06.2019 08.07.2019 1,112942978
Сбербанк-п 23.05.2018 26.06.2018 21.07.2018 0,936437635
Сбербанк-п 11.05.2017 14.06.2017 09.07.2017 1,017492563
Сбербанк-п 11.05.2016 14.06.2016 09.07.2016 1,101864592
Сбербанк-п 12.05.2015 15.06.2015 10.07.2015 0,995812419
Сбербанк-п 14.05.2014 17.06.2014 12.07.2014 1,042997818
Сбербанк-п 08.03.2013 11.04.2013 06.05.2013 0,997301095
Сбербанк-п 09.03.2012 12.04.2012 07.05.2012 0,924053861
Сбербанк-п 12.03.2011 15.04.2011 10.05.2011 1,010644958
Сбербанк-п 13.03.2010 16.04.2010 11.05.2010 0,796937418
Сбербанк-п 04.04.2009 08.05.2009 02.06.2009 2,893620094
Сбербанк-п 04.04.2008 08.05.2008 02.06.2008 1,073578067
Сбербанк-п 08.04.2007 12.05.2007 06.06.2007 0,877649005
Сбербанк-п 25.03.2006 28.04.2006 23.05.2006 0,958642001
Сбербанк-п 03.04.2005 07.05.2005 01.06.2005 1,059276282
Сбербанк-п 28.03.2004 01.05.2004 26.05.2004 1,049810801
Сбербанк-п 06.04.2003 10.05.2003 04.06.2003 1,161792898
Сбербанк-п 02.04.2002 06.05.2002 31.05.2002 1,099316569

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

Поставим нашей модели ещё одну задачу по анализу данных:

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

Будем рассматривать акции, по которым было хотя бы 5 случаев выплаты дивидендов. Получившийся хит-парад приведён ниже. Отметим, что результат имеет ценность скорее всего только с точки зрения задачи на анализ данных, но не как практическое руководство к инвестированию.
Акция Количество
случаев выигрыша
более 10% годовых
сверх инфляции
Количество
случаев
выплаты
дивидендов
Доля
случаев
победы
Средний коэфф. роста
Лензолото 5 5 1 1,320779017
МРСК СЗ 6 7 0,8571 1,070324870
Роллман-п 6 7 0,8571 1,029644533
Россети ап 4 5 0,8 1,279877637
Кубанэнр 4 5 0,8 1,248634960
ЛСР ао 8 10 0,8 1,085474828
АЛРОСА ао 8 10 0,8 1,042920287
ФСК ЕЭС ао 6 8 0,75 1,087420610
НМТП ао 10 14 0,7143 1,166690777
КузбТК ао 5 7 0,7143 1,029743667

Из проведённого анализа рынка акций можно сделать такие выводы:
1) Проверено, что заявленная в материалах брокеров, инвестиционных компаний и прочих заинтересованных лиц доходность акций выше депозитов и инвестиционной недвижимости имеет место быть.
2) Волатильность рынка акций очень высокая, но на долгий срок с существенной диверсификацией портфеля вкладываться можно. Ради добавочных 13% налогового вычета при инвестиции на ИИС открывать для себя рынок акций вполне целесообразно и сделать это можно, в том числе, в Сбербанке.
3) Исходя из анализа результатов за прошлые периоды найдены лидеры по стабильной высокой доходности и по выгодности входа-выхода в окрестности даты выплаты дивидендов. Однако результаты не такие уж однозначные и руководствоваться только ими в своём инвестировании не стоит. Это были примеры задач на анализ данных.

Итого:
Полезно вести учет своего имущества, а также доходов и расходов. Это помогает в финансовом планировании. Если удаётся копить деньги, то есть возможности инвестировать их под ставку выше инфляции. Анализ данных из озера данных Сбербанка показал, что депозиты ежегодно приносят 2%, сдача квартир в аренду 4,5%, а российские акции около 10% свыше инфляции при наличии существенно больших рисков.

Автор: Михаил Гричик, эксперт профессионального сообщества Сбербанка SberProfi DWH/BigData.

Профессиональное сообщество SberProfi DWH/BigData отвечает за развитие компетенций в таких направлениях, как экосистема Hadoop, Teradata, Oracle DB, GreenPlum, а также BI инструментах Qlik, SAP BO, Tableau и др.
Подробнее..

Все научились программировать. А дальше-то что?

10.03.2021 10:18:02 | Автор: admin

Где-то в мире живёт Серёжа тридцатилетний продавец обуви и отец троих детей.

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

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


Вводные: Серёжа любым способом научился программировать. Что ему делать дальше?

Ничего не менять

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

Главное смириться с тем, что продавать туфли придётся ещё долго.

Кому подходит: всем.

Минусы: туфли сами себя не продадут.

Пробовать себя во всём

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

Так, надо попробовать вон тот форк переиздания пятой версии реакта, и NoSQL хвалят, а ещё Svelte неожиданно пробивается в топы. С другой стороны есть друзья с проектами на Wordpress и разработка плагинов для плагинов jQuery на фрилансе. Там всё понятно, да и PHP не очень сложный.

Так-то оно так. Но здесь важно составить план, что и в каком порядке изучать (например, никогда не прикасаться к Java), иначе можно оказаться в ситуации, когда вы знаете всего понемногу, но нигде не дотягиваете даже до джуна. Хотя даже здесь есть выход можно пойти менеджером проектов.

Джейсон Стэтхем, II в. до н.э.Джейсон Стэтхем, II в. до н.э.

Кому подходит: всем, кому не понравилось, что 1+1 не равно двум в каждый из разов.

Минусы: каша в голове, если работать без плана.

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

Углубляться в технологию

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

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

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

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

искала стажировку

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

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

сделала бы сайт для какой-то благотворительной штуки

откликалась бы на кучу вакансий с тестовыми и делала бы все тестовые

сделала бы свой какой-то проект.

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

Лера Зелёная, продюсер цифровых продуктов HTML Academy

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

Минусы: за них скорее всего не заплатят.

Готовиться к собеседованиям

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

Опытные разработчики иногда ходят по компаниям даже без мысли устраиваться туда. Это как спорт знаете, людей посмотреть, себя показать (или вдруг денег предложат побольше). А вот новичкам полезно разобраться, что происходит на собеседованиях, о чём спрашивают и чего говорить вообще никогда-никогда нельзя. Например, никогда не говорите, что вы 1С-программист, если не ищете работу 1С-программиста.

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

Короткий список дел такой:

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

  • Если дадут, то брать тестовые задания и делать их как можно быстрее.

Кому подходит: тем, кто уже готов ко всем этим взрослым деловым переговорам о работе.

Минусы: нужно много времени.

Учиться на работе

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

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

Кому подходит: всем, кто уже прям готов.

Минусы: хорошие туфли всё равно стоят дорого, придётся потрудиться.

И кажется, этого на первых порах будет достаточно.


Любой путь начинается с первого шага. Во фронтенде можно начать с бесплатных тренажёров по основам HTML и CSS или с курса Профессиональная вёрстка сайтов. А с промокодом SKUCHNO цена станет ещё приятнее.

Подробнее..

Перевод Почему в мире так много отстойного ПО

17.05.2021 14:19:21 | Автор: admin
Мы буквально окружены отстойным программным обеспечением. Пенсионные фонды спотыкаются об написанные десятки лет назад пакетные скрипты с ошибочными допущениями. Из кредитных организаций утекает более сотни миллионов номеров социального обеспечения и других конфиденциальных данных. И это ещё не говоря о куче забагованного и раздражающего ПО, создаваемых и мелкими поставщиками, и крупными корпорациями.

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

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


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

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

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



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

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

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

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

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

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



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

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

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

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



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

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

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

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



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

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

image
Подробнее..

Принцип подстановки Барбары Лисков (предусловия и постусловия)

28.05.2021 00:20:41 | Автор: admin

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

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

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

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

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

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

<?phpclass Customer{    protected float $account = 0;    public function putMoneyIntoAccount(int|float $sum): void    {        if ($sum < 1) {            throw new Exception('Вы не можете положить на счёт меньше 1$');        }        $this->account += $sum;    }}class  MicroCustomer extends Customer{    public function putMoneyIntoAccount(int|float $sum): void    {        if ($sum < 1) {            throw new Exception('Вы не можете положить на счёт меньше 1$');        }        // Усиление предусловий        if ($sum > 100) {             throw new Exception('Вы не можете положить на больше 100$');        }        $this->account += $sum;    }}

Добавление второго условия как раз является усилением. Так делать не надо!

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

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

Этот пример показывает, как расширение допускается, потому что метод Bar->process() принимает все типы параметров, которые принимает метод в родительском классе.

<?phpclass Foo{    public function process(int|float $value)    {       // some code    }}class Bar extends Foo{    public function process(int|float|string $value)    {        // some code    }}

Пример ниже показывает, как дочерний класс VIPCustomer может принимать в аргумент переопределяемого метода putMoneyIntoAccount более широкий (более абстрактный) объект Money, чем в его родительском методе (принимает Dollars).

<?phpclass Money {}class Dollars extends Money {}class Customer{    protected Money $account;    public function putMoneyIntoAccount(Dollars $sum): void    {        $this->account = $sum;    }}class VIPCustomer extends Customer{    public function putMoneyIntoAccount(Money $sum): void    {        $this->account = $sum;    }}

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

Постусловия не могут быть ослаблены в подклассе

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

<?phpclass Customer{    protected Dollars $account;    public function chargeMoney(Dollars $sum): float    {        $result = $this->account - $sum->getAmount();        if ($result < 0) { // Постусловие            throw new Exception();        }        return $result;    }}class  VIPCustomer extends Customer{    public function chargeMoney(Dollars $sum): float    {        $result = $this->account - $sum->getAmount();        if ($sum < 1000) { // Добавлено новое поведение            $result -= 5;          }               // Пропущено постусловие базового класса              return $result;    }}

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

Сюда-же можно отнести и Ковариантность, которая позволяет объявлять в методе дочернего класса типом возвращаемого значения подтип того типа (ШО?!), который возвращает родительский метод.

На примере будет проще. Здесь в методе render() дочернего класса, JpgImage объявлен типом возвращаемого значения, который в свою очередь является подтипом Image, который возвращает метод родительского класса Renderer.

<?phpclass Image {}class JpgImage extends Image {}class Renderer{    public function render(): Image    {    }}class PhotoRenderer extends Renderer{    public function render(): JpgImage    {    }}

Таким образом в дочернем классе мы сузили возвращаемое значение. Не ослабили. Усилили :)

Инвариантность

Здесь должно быть чуть проще.

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

Инварианты это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта.

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

<?php class Wallet{    protected float $amount;    // тип данного свойства не должен изменяться в подклассе}

Здесь также стоит упомянуть исторические ограничения (правило истории):

Подкласс не должен создавать новых мутаторов свойств базового класса.

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

<?phpclass Deposit{    protected float $account = 0;    public function __construct(float $sum)    {        if ($sum < 0) {            throw new Exception('Сумма вклада не может быть меньше нуля');        }        $this->account += $sum;    }}class VipDeposit extends Deposit{    public function getMoney(float $sum)    {        $this->account -= $sum;    }}

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

В таком случае стоит рассмотреть добавление мутатора в базовый класс.

Выводы

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

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

Надеюсь, было полезно.

Источники

  1. Вики - Принцип подстановки Барбары Лисков

  2. Metanit

  3. PHP.watch

  4. Telegram канал, с короткими заметками

Подробнее..

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

31.03.2021 10:06:06 | Автор: admin

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

История, описанная ниже, перевод статьи с Medium.com , которая вызвала бурное обсуждение внутри коллектива моей компании (я даже сохранил для вас некоторые комментарии). Но, я уверен, рассказ покажется до боли знакомым очень многим читателям Хабр.

Каково это быть продакт-менеджером, который противостоит инженерному хаосу?

Начальник отдела разработки созвал всех на собрание:

Послушайте, говорит он, у нас серьезное отставание от графика. Множество зависимостей не поддерживаются в коде. У нас нет тестового покрытия. Он делает паузу, как вам кажется, чересчур драматическую и продолжает: Нам нужно переписать код.

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

Что вы имеете в виду под переписать код? спрашиваете вы.

Глава отдела разработки пожимает плечами: Мы используем мультитул версии 1.6, говорит он, но версия 4.3 была выпущена на прошлой неделе, поэтому мы должны быть как минимум на версии 4.x.

"Хорошо, скажете вы, повлияет ли это на фичи, которые мы уже добавили в список?"

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

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

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

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

Вы не собираетесь выпускать пар при помощи пассивно-агрессивных комментариев. Все остальные так делают.

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

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

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

Руководитель мобильной разработки Мегаплана, Игорь Суховерхов

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

Через неделю вы снова встречаетесь. "Как продвигаются дела с кодом?", спрашиваете вы.

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

Проходит месяц. Вы снова встречаетесь:

"Как идут дела?", спрашиваете вы.

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

Они стреляют друг в друга из игрушечного пистолета. И их раздражает, что в офисе нет стола для настольного тенниса.

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

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

Руководитель мобильной разработки Мегаплана, Игорь Суховерхов

Младший разработчик объясняет несколько преимуществ Spasm. Это MVVM(Model-view-viewmodel) паттерн, тогда как Doohickey это MVC(model-view-controller) паттерн, который действительно старомоден. Вы ждете своей очереди и вежливо спрашиваете о некоторых срочных доработках, которые нужно добавить в продукт.

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

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

Ой, дружище, говорит младший разработчик, это такая головная боль.

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

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

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

1) Делайте всё вовремя и не затягивайте с обновлениями.

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

3) Считайте все в деньгах. В какой-то момент стоимость поддержки начнет кратно расти это знак, что пора что-то делать.

4) Если проект хочет развиваться и масштабироваться, а его код устарел и не поддерживается еще один сигнал, что пора заняться рефакторингом

Вы смотрите на экран. Но даже на надеетесь увидеть там законченную работу.

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

Вы смотрите на экран. Но даже не надеетесь увидеть законченную работу.

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

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

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

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

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

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

Руководитель мобильной разработки Мегаплана, Игорь Суховерхов

Креативный директор не согласна: Но ведь сейчас прекрасная возможность! Затем, резко сменив тему: Поговорим об аналитике?

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

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

Я предлагаю использовать Analogix, говорит креативный директор. Начальник отдела разработки быстро изучает этот сервис и видит, что для Spasm есть плагин, так что он за.

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

Глава разработки резко вздыхает, словно он сантехник, который смотрит на протекающую трубу: Это займет кучу времени.

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

Да, говорит он, это добавляет когнитивную нагрузку.

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

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

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

Как вы выполняете другие задачи?, спрашиваете вы: Такие как импорт XML-файлов. Исправление ошибки импорта XML было одной из тех задач, решения которой вам удалось добиться в прошлом спринте.

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

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

Залог хорошего планирования:

1) Точность оценки задач. В scrum это приходит с опытом: каждый новый спринт команда всё лучше и лучше оценивает свои возможности.

2) Дедлайны и сроки работа не должна быть бесконечной.

3) Измеримость конечного результата.

Ну, и конечно хороший проджект-менеджер :)

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

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

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

Руководитель мобильной разработки Мегаплана, Игорь Суховерхов

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

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

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

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

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

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

Подробнее..

Как продать технические задачи бизнесу

23.04.2021 12:19:27 | Автор: admin

Поддерживать высокое техническое качество кода прямая обязанность техлида. Но чтобы этого добиться, зачастую приходится доказывать начальству и заказчикам необходимость вкладывать в улучшение кода силы и время. Как сделать это, не стаптывая в бесконечных согласованиях железные башмаки и не стирая язык до мозолей? Об этом в своем докладе на конференции TechLead Conf 2020 Online рассказал консультант Better Life Company Алексей Дерюшкин.

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

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

Проблемы Алексея выглядели так:

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

  • Бизнес не разбирается в системе и не хочет это делать;

  • Сам техлид не умеет вести переговоры;

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

Почему?! Потому что техлид говорил о каких-то технических вещах, а коллегам из бизнес-подразделений не интересно, как это работает. У них другие цели и задачи: как повысить конверсию, как привлечь больше людей, как сделать классный продукт. И технические детали им не интересны.

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

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

Чем продажа идеи отличается от программирования?

Спойлер: ничем!

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

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

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

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

Пример неудачной продажи

Техлид приходит к владельцу продукта:

  • Привет! Я хочу добавить в этот спринт задачу по рефакторингу.

  • Привет! Нет, в этот спринт мы должны выпустить новую фичу, мы уже обещали.

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

  • Ну, делайте сразу нормально, кто ж мешает?

Это результат неподготовленного диалога.

Давайте разберем ошибки типичного диалога техлида с бизнесом:

  • Птичий язык: владельцу продукта непонятно, о чем мы говорим;

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

  • Потребность убедить в своей правоте, а не помочь решить проблему бизнеса.

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

Система продаж Джордана Белфорта

Есть замечательная цитата:

В фильме Волк с Уолл-стрит с Леонардо ДиКаприо прототипом главного персонажа является Джордан Белфорт один из самых известных продавцов и автор системы продаж.

На чем основывается его система? Джордан утверждает, что у вас купят идею (а на самом деле купят что угодно: телефон, автомобиль и т.д.), если есть 100% уверенность:

  • В идее (продукте, предложении);

  • В вас, как эксперте;

  • В людях, которых вы представляете.

Рассмотрим бытовой пример: вам продают автомобиль.

  • Идея 71%

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

  • Эксперт 83%

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

  • Команда 78%

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

Если во всех трех пунктах ваша уверенность 100% или близко, то скорее всего, автомобиль вы купите.

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

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

  • Идея 0%

  • Эксперт 100%

  • Команда 100%

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

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

  • Идея 100%

  • Эксперт 0%

  • Команда 100%

В случае, когда вы не хотите связываться с АвтоВАЗом, и LADA Granta не ваша мечта, будет так:

  • Идея 100%

  • Эксперт 100%

  • Команда 0%

То есть если даже один параметр будет не 100%, а 30-40% и ниже, вы не убедите человека купить автомобиль. А на месте техлида не продадите идею сделать тесты, что-то автоматизировать или провести рефакторинг.

Важный момент по поводу 100 баллов из 100. Вы можете только предполагать, каковы цифры на самом деле. Странно было бы на переговорах говорить человеку: А ну, скажи мне: от 0 до 100, насколько ты во мне уверен?. Это просто метафора, а не цифры, которые можно использовать в разговоре.

Как добиться уверенности на 100\100\100?

Идти по алгоритму.

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

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

Алгоритм продажи чего угодно, в том числе идей

Первые четыре секунды

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

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

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

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

Раппорт

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

Отсутствие раппорта очень хорошо описывает картинка:

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

Сбор информации

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

Давайте посмотрим на три пункта:

  • Боли ярко окрашенные негативные эмоции;

  • Выгоды позитив;

  • Цели и задачи более нейтральные вещи.

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

Основная презентация и предложение о покупке

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

Строим презентацию по схеме:

  • Как ваша идея поможет собеседнику?

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

  • Визуализируем ужасное будущее;

  • Визуализируем хорошее будущее;

  • Если это нужно, даем МИНИМУМ технических деталей;

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

  • Называем цену.

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

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

Мне кажется, это хорошая идея. Что скажешь?

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

Перенаправление на рост уверенности.

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

  • Спросите, что смущает, и развейте страх

Если у вас не купили, значит, нет уверенности. Надо выяснить, где провал, почему 50% вместо 100%. И постараться это починить. Возможно, собеседник скажет напрямую:

  • Я не уверен, что это поможет.

Об идее, скорее всего, можно говорить прямо. Но вряд ли человек скажет:

  • Я тебе не доверяю, как специалисту.

Или:

  • Мне кажется, твоя команда не справится.

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

Снижение порога действия

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

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

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

  • Просто кивни, мы все сделаем сами.

  • Тебе ничего не надо будет делать.

  • Мы уже попробовали, и вот результат...

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

Усиление боли

Это визуализация страшного будущего, в котором ваш собеседник не согласился на вашу идею:

  • Если ничего не поменяется, то...

  • Знаешь ли ты, что будем, если мы этого не сделаем?

  • Через год станет

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

Закрытие сделки

Это опять передача мяча собеседнику:

  • Что дальше?

  • Ну что, попробуем?

  • Ты согласен, что...?

Это прямые вопросы, которые вы задаете собеседнику: Ну, как? Берем работу в следующий спринт? Ставим в план на следующий месяц?. Но эти вопросы часто просто забывают задать. Мы вроде бы обсудили идею и просто ждем, когда человек сам скажет: Ладно, давай. Я буду делать то-то и то-то.

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

Пример

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

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

Диалог был примерно такой:

  • Привет! Я хочу обсудить одну техническую задачу, которая может ускорить время разработки задач и починки ошибок. Есть 10 минут, чтобы обсудить сейчас?

  • Давай.

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

Дальше идет презентация:

Смотри, сейчас у нас нет автотестов, и каждую фичу перед выпуском мы тестируем руками, ЭТО ДОЛГО. Я с ребятами УЖЕ ПОСЧИТАЛ, что если мы закроем 15% функционала тестами, то разработка каждой новой фичи УСКОРИТСЯ НА 2 ДНЯ. Благодаря этому мы сможем сделать на 10 фич больше до дедлайна. Мы все сделаем сами, надо просто взять в этот и следующий спринты задачи по разработке тестов. За этот месяц мы сделаем меньше фич, НО зато потом каждую будем делать быстрее. А еще это поможет быстрее чинить баги.

Что мы здесь видим? Во-первых, в презентации были указаны конкретные цифры. 15% функционала, 2 дня и 10 фич.

Во-вторых, был минимум технических деталей, а конкретно одна: 15% функционала надо закрыть. Цифра на самом деле не важна это может быть и 10%. Владельцу продукта важнее, что разработка новой фичи ускорится, или что баги будут чиниться быстрее.

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

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

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

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

  • Что скажешь?

Это пример, как можно по такому алгоритму проводить продажи идей.

Полезные фишки

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

  • НО наоборот;

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

  • Потому что;

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

  • Визуализация;

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

  • Двойная петля;

Если вы видите, что собеседник сомневается (сначала у него есть один аргумент против, потом появляется другой, третий), скажите ему прямо: Мне кажется, что ты не уверен в идее, которую я принес. Расскажи, почему?. Так он попадает в двойную петлю: либо говорит: Нет-нет, я уверен, и начинает сам себе продавать уверенность, либо говорит: Да, я не уверен, и тогда дает вам аргументы, с которыми вы сможете работать.

  • Прямолинейность.

То, что вы честно и открыто ведете диалог, всегда подкупает.

Подводя итог

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

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

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

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

TechLead Conf 2021 пройдет 30 июня 2021и 1 июля 2021 в московской Radisson Slavyanskaya. Расписание можно посмотреть здесь.

Билеты на конференцию вы можете приобрести уже сейчас. Спешите: 1 мая цена станет выше!

Подробнее..

Перевод 3 самых больших факапа разработчика

30.04.2021 16:23:36 | Автор: admin

В оригинальной статье на сайте Medium, хотя и написанной от лица мужского пола, можно сказать от библейского первого человека Адама, в пример топового разработчика приводится девушка, которая в 11 лет сделала свой сайт, а к 23-м годам стала миллиардером. Судя по тому, что у нас в компании девушки-разработчики большая редкость (их всего две), а мы типичная IT-компания, для адаптации к местным реалиям пришлось превратить ее в парня. Получилось почти как у Киплинга. Помните, пантера Багира в оригинальных книгах был мачо-мальчиком, а в советском мульте превратился в супердевочку с голосом актрисы Касаткиной.

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

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

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

Тысячи URL-адресов были потеряны

Работая в крупном финансовом учреждении, я разработал систему для очистки неиспользуемых маршрутов на сетевом уровне F5. Пул маршрутов F5 мог поддерживать примерно 5000 URL-адресов. Затем он засорялся. Моя система автоматизировала процесс мониторинга трафика по этим URL-адресам, уведомляла владельцев о неиспользуемых ресурсах и в конечном итоге очищала их. Она не давала системе F5 выйти из строя и освобождала от ручной обработки операций.

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

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

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

Выяснилось, что большинство ресурсов, неактивных в течение недели, остаются такими целый месяц. Другими словами, важные приложения не бездействуют в течение недели. Так что в конечном итоге ущерб был управляемым; из 1000 удаленных URL лишь несколько команд пожаловались на проблему. Однако негативная обратная связь и стресс от проблемы сильно повлияли на меня и моих менеджеров. Особенно, когда мы только узнали о проблеме и не осознавали масштабов ущерба. В результате мы развернули военный штаб: вооруженная до зубов команда была готова в любой момент вручную воссоздать потерянные ресурсы или удаленные URL'ы.

Как это могло случиться?!

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

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

Что я вынес из этой ситуации?

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

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

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

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

База данных и учетные записи должны быть доступны по умолчанию только для чтения, должны иметь четкие стратегии резервного копирования и восстановления. Например, на моей следующей работе разработчик случайно удалил файлы из корзины prod S3. Если бы у нас не было стратегии управления версиями S3, которую я установил всего за неделю до этого (она отключена по умолчанию, черт возьми, Amazon!), мы могли потерять ее навсегда.

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

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

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

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

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

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

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

Как это могло случиться!?

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

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

Что я вынес из этой ситуации?

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

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

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

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

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

Однако к июлю стало ясно, что нам нужно или сокращать команду, или наш корабль пойдет ко дну. В 11:30 генеральный директор лично отправил мне сообщение в Slack со зловещей просьбой позвонить ему в 12:00. В 12:15 меня уволили. Цитирую: Адам, мы вынуждены немедленно отпустить вас. Мы считаем, что вы хорошо поработали, однако из-за сложных условий мы сокращаем персонал. Я был одним из 15 человек, бесцеремонно уволенных в тот день, без предупреждения, выходного пособия, даже без пяти минут, чтобы попрощаться с командой (учетная запись Slack была отключена во время разговора).

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

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

Как это могло случиться!?

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

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

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

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

Что я извлек из этой ситуации?

Меня никуда не брали. Особенно обидно было, когда я почти получил работу мечты в компании, которая занимается AV-технологиями, но мне отказали на последнем этапе собеседования (второй год подряд). В конце концов я устроился java-разработчиком в посредственную компанию. Я быстро понял, что скучное ПО это круто: у него простые требования и давно сформировавшийся пул пользователей. Такие вещи, как эта кнопка не работает, легко исправить, и для этого не требуется докторская степень или годы планирования. На самом деле очень приятно пользоваться простыми и максимально доступным инструментарием и получать за это благодарности от реальных людей.

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

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

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

P.S. Позор тем компаниям, которые оставляют соискателей без обратной связи.

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

Подробнее..

Как писать читаемый код

19.01.2021 10:15:06 | Автор: admin

Бывает, что посмотрев на старый код, мы говорим: Его проще переписать, чем поменять. Печально, если речь идет о нашем собственном коде, с такой любовь написанном несколько лет назад. Head of Developer Relations в Evrone Григорий Петров в своем докладе на TechLead Conf 2020 разобрал проблемы, которые приводят к такой ситуации, и рассказал, как бороться с Software complexity problem.

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

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

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

Художник рисует мазками

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

Можно сказать, что программисты тоже пишут код своеобразными мазками: идентификатор за идентификатором, оператор за оператором, expression, statement, строчка за строчкой получаются творения в 10, в 100, в 1000 строк кода.

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

Поэтому одна из главных проблем, существующих в разработке софта это то, что мы называем Software complexity problem, что можно перевести на русский как проблема сложности программ.

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

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

Проблемы программирования

  • Нулевая цена копирования;

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

  • Нет понимания как правильно;

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

  • Отсутствие фундаментального образования;

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

  • Сложно посмотреть, как делают другие.

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

Интуитивное мышление, когнитивные искажения

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

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

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

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

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

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

Борьба со сложностью

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

Но сложность можно перераспределить! Именно об этом пойдет речь в статье.

  • Память;

Гиппокамп это часть мозга, которая, предположительно, имеет отношение к формированию памяти. Известно, что когда выходит из строя гиппокамп, ломается память.

Как это происходит, не совсем ясно. Но существует такая закономерность, как Кошелек Миллера: когда человек смотрит на новые для себя объекты, в среднем, он может удержать в фокусе внимания от 5 до 9 из них.

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

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

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

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

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

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

Если программист много лет пишет на Python и использует requests, он к ним привыкает. Типичные конструкции использования requests как сделать запрос, как передать и получить JSON, как решать вопросы скорости и задержек ему привычны. Код, который использует библиотеку, становится для программиста читаемым. В таком коде больше нет сложности. По крайней мере, для этого конкретного программиста.

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

Точно также работает стандарт кодирования, coding style. Код разработчиков может быть читаемым друг для друга, но только если они хотя бы несколько месяцев поживут с ним. Нейрофизиология утверждает, что несколько месяцев и несколько сотен повторений нужны нашей памяти, чтобы выстроить долговременные связи long-term potentiation, чем бы они ни были.

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

Но память это долго. Это самый простой, но и самый длительный по времени способ борьбы со сложностью.

  • Декомпозиция;

Второй по популярности способ это декомпозиция на части по 5 элементов.

Посмотрим на эволюцию типичной сферической программы в вакууме.

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

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

Через некоторое время, когда абстракции языка программирования исчерпывают себя, и строчек становится несколько десятков тысяч, разработчики начинают с интересом смотреть в сторону DSL: YAML, JSON, Ruby, XML и т.д.

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

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

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

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

  • Метаинформация.

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

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

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

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

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

или Мосгорводоканалстрой это одна сущность для нашего мозга.

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

В Go все очень тяжело со сложностью, потому что язык практически не предоставляет синтаксического сахара. В книге Как писать на языке программирования Go есть параграф про эволюцию идентификаторов. Там написано о том, как бороться с когнитивной сложностью в коде. Выбирая имя для идентификатора, авторы предлагают смотреть на то, что находится рядом с этим идентификатором. Если там что-то очень простое (функция, 2-3 строчки кода, которые очевидны), то идентификатор может быть i или v:

v => users => admin_users

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

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

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

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

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

Например:

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

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

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

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

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

В последние 5-10 лет в динамические языки программирования пришли типы. Они выполняют роль своеобразных капканов для ошибок. Когда программист пишет код, он может воспользоваться Gradual подходом современных языков. И там, где сложность повышена, добавить типы, чтобы в коде расставились несколько капканов.

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

Gradual подход к перераспределению сложности

Gradual подход к написанию читаемого кода по больше части вращается вокруг цифры 5.

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

  • Gradual decomposition;

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

  • Gradual meta information;

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

  • Gradual typing.

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

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

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

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

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

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

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

Конференция, полностью посвященная инженерным процессам и практикам, TechLead Conf 2021 пройдет 12и 13 апреля. Билеты можно приобрести здесь. Вы можете успеть купить их до повышения цены!

А пока мы все ждем апреля, приглашаем вас на Quality Assurance Webinar. На нем поговорим о пирамиде тестирования, узнаем, как найти UI тесты, которые легко могут быть перенесены на более низкие уровни, и разберемся в инфраструктуре тестирования в браузерах.

Мероприятие начнется 21 января в 18:00 мск. До встречи!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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