Сайт Романа ПарпалакаБлог

Существует ли идеальный код, или новый разработчик всегда хочет всё переписать?

17 ноября 2024 года, 13:30

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

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

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

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

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

    Оставить комментарий

Технология отбеливания пластика Retrobright

15 августа 2024 года, 10:47

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

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

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

Я дождался 2 метра светодиодной ленты с Алиэкспресса и купил гель с концентрацией перекиси водорода 12%. Протестировать способ решил на телефонной трубке, обмотав ее светодиодной лентой.

Ближе к концу процесса гель слегка вспенивается от выделяющегося кислорода. Но как показывает практика, на результат это не влияет.

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

Проблема с неравномерностью засветки была самой серьезной. Чтобы ее избежать, я прикрепил ленту змейкой к листу металла и размещал такой импровизированный светильник на небольшом расстоянии от пластика.

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

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

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

В целом результатом я доволен, могу рекомендовать к повторению. Я купил 60 миллилитров геля для обесцвечивания волос, для телефона этого хватило с небольшим запасом. Гель наносил старой зубной щеткой на предварительно вымытую поверхность. Ленту запитывал током в 900 миллиампер, при этом напряжение на ней было около 11,5 вольт. Работайте в перчатках и очках, так как концентрированная перекись водорода опасна.

    Оставить комментарий

Как отремонтировать убитую дискету

11 августа 2024 года, 12:48

Ютуб порекомендовал ролик о том, что обычные дискеты на 1,44 мегабайта можно отформатировать на больший объем. Я помню один такой способ: взять Дос Навигатор и задать при форматировании объем в 1,6 мегабайт. Дискеты после этого нормально работали на повышенной емкости. В ролике об этом способе, кстати, не рассказали.

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

Когда какая-то область гибкого диска повреждена, например, поцарапана, при чтении и записи в нее возникают ошибки. Обычно при форматировании сбойные кластеры помечаются в таблице размещения файлов (File Allocation Table, FAT) особым образом. При этом доступное место на дискете сокращается, но ее всё равно еще можно использовать. Однако если повреждена дорожка номер 0, в которую записывается и загрузочный сектор, и сама таблица размещения файлов, операционная система полностью отказывается работать с такой дискетой. Отформатировать ее невозможно.

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

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

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

А вот другой диск еще держится. Около основания видны прилипшие к остаткам клеевого слоя пылинки и ворсинки:

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

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

    1 комментарий

Применение конечных автоматов в программировании

9 июля 2024 года, 11:11

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

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

Проблемы в примере без конечных автоматов

Когда-то давно комментарии в S2 имели всего два состояния, закодированных флагом shown. Комментарий создавался сразу опубликованным (shown = 1), и позднее его можно было скрыть (shown = 0).

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

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

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

А в режиме с включенной предварительной проверкой появляется новое состояние:

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

$$\usetikzlibrary{arrows} \begin{tikzpicture}[node distance=4cm,font=\sffamily] \tikzset{ mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.7em, text width=7em,text centered}, myarrow/.style={->, >=latex', shorten >=1pt, shorten <=2pt,thick} } \node[mynode,fill=yellow!10] (moder) {\shortstack{На проверке\\\tt shown=0\\\tt sent=0}}; \node[mynode, right of=moder,fill=green!10] (pub) {\shortstack{Опубликован\\\tt shown=1\\\tt sent=1}}; \node[mynode, right of=pub,fill=gray!10] (hidden) {\shortstack{Скрыт\\\tt shown=0\\\tt sent=1}}; \draw[myarrow] (moder) to[in=130,out=50] node[above] {рассылка} (pub); \draw[myarrow] (pub) to[in=130,out=50] (hidden); \draw[myarrow] (hidden) to[in=-50,out=-130] (pub); \end{tikzpicture}$$

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

Когда я увидел на практике необходимость такого перехода, то запрограммировал новую кнопку «оставить скрытым и не рассылать», которая (внимание!) изменяла значение флага sent с 0 на 1 без фактической рассылки комментариев. Таким образом, поменялся смысл флага sent: раньше он указывал на то, что комментарий был разослан, а теперь указывает на отсутствие необходимости разослать комментарий.

$$\usetikzlibrary{arrows} \begin{tikzpicture}[node distance=4cm,font=\sffamily] \tikzset{ mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.7em, text width=7em,text centered}, myarrow/.style={->, >=latex', shorten >=1pt, shorten <=2pt,thick} } \node[mynode,fill=yellow!10] (moder) {\shortstack{На проверке\\\tt shown=0\\\tt sent=0}}; \node[mynode, right of=moder,fill=green!10] (pub) {\shortstack{Опубликован\\\tt shown=1\\\tt sent=1}}; \node[mynode, right of=pub,fill=gray!10] (hidden) {\shortstack{Скрыт\\\tt shown=0\\\tt sent=1}}; \draw[myarrow] (moder) to[in=140,out=40] node[above] {\shortstack{Одобрить\\ (+рассылка)}} (pub); \draw[myarrow] (moder) to[in=-120,out=-60] node[below] {Оставить скрытым и не рассылать} (hidden); \draw[myarrow] (pub) to[in=140,out=40] node[above] {Скрыть} (hidden); \draw[myarrow] (hidden) to[in=-40,out=-140] node[below,pos=0.7] {Опубликовать} (pub); \end{tikzpicture}$$

В итоге мы получили следующие проблемы:

Последний пункт особенно важно осознать: значения shown=1 и sent=0 можно получить либо в результате ошибки в коде, либо прямым редактированием базы данных. Выходит, такая модель данных может кодировать несуществующее состояние, и это свидетельствует об ошибке проектирования.

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

Как хранить и обрабатывать статусы

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

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

$$\usetikzlibrary{arrows,positioning} \begin{tikzpicture}[font=\sffamily] \tikzset{ block/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.7em, text width=8em,text centered}, myarrow/.style={->, >=latex', shorten >=1pt, shorten <=2pt,thick} } \node[block,fill=yellow!10] (moder) {\shortstack{На проверке\\\tt status=pending}}; \node[block, above right=0cm and 3cm of moder,fill=green!10] (pub) {\shortstack{Опубликован\\\tt status=published}}; \node[block, below right=0cm and 3cm of moder,fill=gray!10] (hidden) {\shortstack{Скрыт\\\tt status=hidden}}; \draw[myarrow] (moder) to[in=180,out=30] node[above] {\shortstack{Одобрить\\ (+рассылка)}} (pub); \draw[myarrow] (moder) to[in=180,out=-30] node[below] {Отклонить} (hidden); \draw[myarrow] (pub) to[in=120,out=-120] node[left] {Скрыть} (hidden); \draw[myarrow] (hidden) to[in=-60,out=60] node[right] {Опубликовать} (pub); \end{tikzpicture}$$

После этого везде в коде вместо проверки двух разных свойств shown и sent нужно проверять значение одного свойства status. Например, в запросе для вывода комментариев читателям блога нужно писать не WHERE shown=1, а WHERE status='published'.

Сходу программисту может быть непонятно, какой набор значений должен быть у поля «статус». Но это не значит, что у моделируемых объектов нет набора состояний и возможных переходов между ними. Если их не выявить, код окажется более сложным и запутанным, чем мог бы быть. А если статус выделен правильно, получаем такие преимущества:

Польза для общения с бизнесом

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

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

Состояния в больших системах

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

Для примера представьте, что вы отправляете заявку на ипотечный кредит через личный кабинет на сайте банка. Пока вы заполняете анкету, фотографируете и прикрепляете документы, заявка находится в статусе «черновик». При отправке заявки вы переводите ее в статус «на проверке». Одни сервисы начинают выполнять автоматические проверки. В других сервисах происходит ручная проверка прикрепленных фотографий. Личный кабинет скорее всего не будет знать о таких мельчайших подробностях, как состояние запроса в ФНС на получение вашего ИНН (запрос еще не отправлен, ожидается ответ, ответ получен). Но если в ходе проверки были выявлены проблемы, личный кабинет их отобразит. Например, если паспорт прошел проверку, а справка по форме 2-НДФЛ — нет, вы увидите это на сайте с дальнейшими инструкциями. Вся заявка перейдет в статус «на доработке» с ожиданием ваших действий.

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

Состояния в append-only системах

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

В append-only системах поле «статус» выносится в отдельную таблицу, связанную с первоначальной. На каждое изменение статуса в этой отдельной таблице создается запись с новым значением статуса. Чтобы понять, в каком статусе находится сущность, надо взять последнюю запись. В зависимости от бизнес-процесса в эту таблицу удобно записывать другие связанные значения. В примере выше с проверкой документов из кредитной заявки связанная таблица будет хранить результат проверки специалистом: кто именно проверил документ, в какой момент, какое было решение (например, документ одобрен, отклонен или требуется мнение еще одного специалиста). Здесь последнее принятое решение как раз и будет статусом самого документа.

Зачем вообще делать append-only таблицы, если для определения статуса нужно взять не просто значение из колонки, а доставать записи из соседней таблицы и находить последнюю из них? Действительно, чтобы понимать статус в ходе обработки документа, нужно провести дополнительные операции при выполнении и вообще потратить время на их программирование. С другой стороны мы будем хранить все исходные данные о событиях в системе. Есть и другие положительные моменты:

    Оставить комментарий

Можно ли надежно определить, по какому адресу открыли сайт?

17 июня 2024 года, 22:37

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

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

Проблема у собеседника проявлялась в том, что после отправки формы с логином и паролем редирект происходил на адрес типа https://example.com:80/some_forum_url. Ответ не приходил, потому что на порту 80 никто не обрабатывал https-запросы, так как веб-сервер ожидал их на стандартном порту 443.

PunBB устроен так, что в момент установки URL форума записывается в специальную переменную в файле настройки. Сама эта переменная была установлена правильно, порта в ней не было: https://example.com/. Но именно после входа неверный порт откуда-то появлялся.

Я поискал по коду форума «80» и нашел такую строчку:

$port = (isset($_SERVER['SERVER_PORT'])
   && (
      ($_SERVER['SERVER_PORT'] != '80' && $protocol == 'http://')
      || ($_SERVER['SERVER_PORT'] != '443' && $protocol == 'https://')
   ) && strpos($_SERVER['HTTP_HOST'], ':') === false)
   ? ':'.$_SERVER['SERVER_PORT']
   : '';

Здесь код пытается понять по значению серверной переменной $_SERVER['SERVER_PORT'], запущен ли он на нестандартном порту. Я предложил заменить строку на $port = ''. Проблема исчезла.

Оказалось, что на хостинге значение переменной $_SERVER['SERVER_PORT'] было установлено неверно. Оно равнялось 80, хотя сам сайт открывается по стандартному для https порту 443.

Надо сказать, что у меня нет понимания, нужно ли вообще обрабатывать значение $_SERVER['SERVER_PORT']. С одной стороны, если не обработать, то движок получается менее универсальным, он не может определить, что запущен на нестандартном порту. С другой стороны, если обрабатывать, можно столкнуться с некорректной настройкой веб-сервера и ошибками на ровном месте, как в этом случае. Описание похожей проблемы есть на стековерфлоу, так что это не какой-то экзотический случай.

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

    2 комментария

Глюки подключения модема и ошибки мышления

12 июня 2024 года, 20:33

Вспомнил историю, которая хорошо иллюстрирует одну из ошибок мышления: «после — не значит вследствие».

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

В компьютере был модем с заявленной скоростью 33,6 килобит в секунду. Такой скорости подключения я никогда не видел. Настоящая скорость была немного ниже, 28,8 или 31,2 килобита в секунду. По тем временам таким интернетом можно было пользоваться более-менее сносно.

Скорость передачи данных через модем зависит от качества телефонной линии. У меня изредка появлялись ошибки подключения. Я списывал их на плохой контакт в проводе от компьютера к телефонной розетке. Стандартный провод был слишком коротким, и я его удлинил. Когда начинались ошибки подключения, я наклонялся к компьютеру, шевелил провод и разъемы, пытаясь улучшить контакт. После нескольких попыток ошибки пропадали.

Чтобы как-то улучшить ситуацию с качеством подключения, я несколько раз перепаивал провод, применил витую пару, укреплял места пайки, думая, что они со временем «расшатываются». Проблемы исчезали и потом появлялись снова.

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

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

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

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

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

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

    Оставить комментарий

Метод удвоения персонажей

8 мая 2024 года, 20:02

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

Откуда появился метод

Однажды коллега дал мне задачу на движение, якобы с собеседований (см. задачу №1 ниже). Чтобы упростить решение, я использовал метод (назовем его «удвоением персонажей»), о котором узнал из книжки Мартина Гарднера «Математические досуги». В ней сформулирована следующая задача:

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

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

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

Решение задачи оказалось элегантным:

Человек поднимается на высокую гору и, пробыв несколько дней на вершине, спускается вниз. Найдется ли такая точка на тропе, которую оба раза он проходит в одно и то же время суток? Мое внимание на эту задачу обратил психолог из Орегонского университета Р. Хайман, который в свою очередь нашел ее в монографии, озаглавленной «О решении задач» и принадлежащей перу немецкого психолога Дункера. Дункер пишет, что сам он не смог решить задачу, и с удовлетворением отмечает, что никто из тех, кому он ее предлагал, тоже не добился успеха. Далее Дункер говорит о том, что существует много подходов к решению задачи, но, по его мнению, «самым очевидным является следующее объяснение. Пусть в один и тот же день по тропе идут два человека: один из них поднимается вверх, а второй спускается вниз. Они обязательно должны встретиться. Отсюда, как вы сами понимаете, следует, что… при таком подходе неясный вначале смысл задачи вдруг сразу становится совершенно очевидным».

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

Задача №1

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

Решение через систему уравнений

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

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

$${x\over u}={y/4\over v}.$$

Во второй ситуации до встречи поезд проходит сумму расстояний x и y, человек пробегает три четверти y. Аналогично получаем:

$${x+y\over u}={3y/4\over v}.$$

Вычитаем из второго уравнения первое:

$${y\over u}={2y/4\over v}={y\over 2v}.$$

Переменная x исчезла, у как ненулевая величина сокращается. Отсюда $$u=2v$$, то есть поезд в два раза быстрее.

Решение методом удвоения персонажей

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

$$\begin{tikzpicture}[font=\sffamily] \node[fill=green!20] (1) at (1.5,0.5) {1}; \node[fill=blue!20] (2) at (1.5,1) {2}; \draw[->] (1.west) -- ++(-0.5,0); \draw[->] (2.east) -- ++(0.5,0); \node[fill=red!30] at (-4,-0.5) {поезд}; \draw[semithick] (-5,0) -- (8,0); \foreach \x in {0,1.5,3,4.5,6} \draw (\x,0.1) -- (\x,-0.1); \draw[fill=yellow] (0,-0.035) rectangle ++(6,0.07); \end{tikzpicture}$$

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

$$\begin{tikzpicture}[font=\sffamily] \node[fill=green!20] at (0,0.5) {1}; \node[fill=blue!20] at (3,0.5) {2}; \node[fill=red!30] at (0,-0.5) {поезд}; \draw[semithick] (-5,0) -- (8,0); \foreach \x in {0,1.5,3,4.5,6} \draw (\x,0.1) -- (\x,-0.1); \draw[fill=yellow] (0,-0.035) rectangle ++(6,0.07); \end{tikzpicture}$$

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

$$\begin{tikzpicture}[font=\sffamily] \node[fill=green!20] at (0,0.5) {1}; \node[fill=blue!20] at (6,0.5) {2}; \node[fill=red!30] at (6,-0.5) {поезд}; \draw[semithick] (-5,0) -- (8,0); \foreach \x in {0,1.5,3,4.5,6} \draw (\x,0.1) -- (\x,-0.1); \draw[fill=yellow] (0,-0.035) rectangle ++(6,0.07); \end{tikzpicture}$$

Таким образом, поезд в два раза быстрее человека.

Сравнение методов

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

Задача №2

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

Эту задачу разбирали в следующем ролике на канале GetAClass:

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

Решение

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

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

$$\begin{tikzpicture}[font=\sffamily] \node[fill=green!20] (1) at (2,0.5) {1}; \node[fill=blue!20] (2) at (2,1) {2}; \draw[->] (1.east) -- ++(0.5,0); \draw[->] (2.west) -- ++(-0.5,0); \node[fill=red!30] at (2,-0.5) {электричка 1}; \draw[-,semithick] (-5,0) -- (8,0); \foreach \x in {0,2,4,6} \draw (\x,0.1) -- (\x,-0.1); \end{tikzpicture}$$

Через полчаса второй велосипедист, едущий назад, встречает следующую электричку. Запомним, что к этому моменту первый велосипедист проехал полчаса вперед. То есть расстояние между велосипедистами соответствует часу проходимого ими пути, назовем его «велосипедо-часом».

$$\begin{tikzpicture}[font=\sffamily] \node[fill=green!20] at (4,0.5) {1}; \node[fill=blue!20] at (0,0.5) {2}; \node[fill=red!30] at (0,-0.5) {электричка 2}; \draw[-,semithick] (-5,0) -- (8,0); \foreach \x in {0,2,4,6} \draw (\x,0.1) -- (\x,-0.1); \end{tikzpicture}$$

За следующие полчаса электричка проезжает этот «велосипедо-час», а также еще половину «велосипедо-часа», которую проезжает первый велосипедист.

$$\begin{tikzpicture}[font=\sffamily] \node[fill=green!20] at (6,0.5) {1}; \node[fill=blue!20] at (-2,0.5) {2}; \node[fill=red!30] at (6,-0.5) {электричка 2}; \draw[-,semithick] (-5,0) -- (8,0); \foreach \x in {0,2,4,6} \draw (\x,0.1) -- (\x,-0.1); \end{tikzpicture}$$

Теперь мы видим, что за последние полчаса электричка проезжает расстояние, в три раза большее, чем велосипедист, $$u=3v$$.

До второй половины решения задачи не так легко додуматься. Попробую объяснить, как к нему прийти. В условии есть некоторая неизменная величина — расстояние между соседними электричками. С ней проще работать в той системе отсчета, где электрички покоятся. То есть мы сейчас посмотрим на события глазами пассажиров электрички. Первый велосипедист движется в ту же сторону, что и мы, но мы в три раза быстрее. Скорость, с которой мы догоняем велосипедиста, равна двум велосипедным. Второй велосипедист движется на нас, и скорость сближения равна четырем велосипедным. К пассажирам на неподвижных станциях мы приближаемся с собственной скоростью, равной трем велосипедным скоростям. Нам будет казаться, что с этой скоростью они проносятся мимо нас назад. Получается, что одно и то же расстояние S между нами и следующей за нами электричкой первый велосипедист проходит с относительной скоростью 2v за час, второй с относительной скоростью 4v проходит за полчаса, а пассажиры на станции со скоростью 3v «преодолевают» за искомое время t. Запишем в виде уравнений:

$$S=2v\cdot 1,\quad S=4v\cdot{1\over 2},\quad S=3v\cdot t.$$

То, что первое и второе уравнения получились одинаковыми, показывает, что в первой половине решения ошибок не было. Из этих уравнений видно, что $$t=2/3$$ часа, или 40 минут.

Я бы не сказал, что до этого решения задачи додуматься проще всего. Можно было сразу записать систему уравнений с неизвестными скоростями $$(u-v)\cdot 1=(u+v)\cdot 0,\!5=u\cdot t$$ и решить ее относительно t. Но мне надо было на каком-то примере показать, как работает метод удвоения персонажей.

    Оставить комментарий

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

6 апреля 2024 года, 20:03

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

Условие задачи

Нужно найти в строке символы i, b, u, d, перед которыми расположено нечетное количество обратных слешей, например \I или \\\b.

Мотивация

Расскажу, откуда появилась эта задача. В свой поисковый движок Rose я добавил поддержку простейшего форматирования в сниппетах: курсив, жирный шрифт, верхние и нижние индексы. Благодаря этому формула y = ex в сниппете отображается правильно (без форматирования было бы y = ex):

Я не хотел хранить текст сниппетов с частично вырезанными HTML-тегами и придумал свою разметку. Последовательность символов \i включает курсивное начертание, последовательность \I отключает. Аналогично с остальным форматированием. При этом, чтобы хранить сам символ обратного слеша, его нужно продублировать для экранировки (\\). Таким образом, формула из примера выше хранится в сниппетах как \iy\I = e\u\ix\I\U.

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

Решение

Я составил такую регулярку: #\\(?:\\(*SKIP)\\)*\K[ibud]#i. Давайте разберем ее по шагам.

  1. Регулярка начинается с символа обратного слеша. Нужно помнить, что в регулярных выражениях он имеет специальное значение, и сам по себе должен быть экранирован.
  2. Дальше идет группа (?:...) без захвата, то есть ее содержимое не попадает в итоговый массив результатов $matches.
  3. Внутри группы находятся два обратных слеша, а сама группа указана с квантификатором *, означающим её повторение любого количества раз, включая нулевое. Таким образом уже разобранная часть регулярки должна срабатывать на нечетном количестве слешей.
  4. Внутри группы также расположена управляющая последовательность бэктрекинга (*SKIP). Она обозначает некоторую границу и дает инструкции движку регулярных выражений, чтобы он не переходил эту границу при переборе возможных повторений, задаваемых квантификатором *, а также сразу переходил к ней в исходной строке, если было только частичное совпадение с регуляркой. Без этой управляющей последовательности мы бы получили ложное совпадение на строке \\i с четным количеством слешей. Действительно, в ней на первом проходе, начиная с первого символа \\i, совпадения нет из-за четного количества слешей. Но дальше мы получим совпадение, начиная со второго символа: \\i. (*SKIP) же задает границу между вторым слешем и следующим символом, поэтому движок регулярок при работе не будет проверять совпадение со второго символа, а сразу перейдет к третьему. В англоязычной литературе для подобных управляющих последовательностей используется термин Backtracking Control Verbs, среди них есть и другие полезные возможности.
  5. Следующей идет последовательность \K. Она убирает из результатов общего совпадения всё, что было до нее. Таким образом, в $matches[0] попадет только оставшаяся часть совпадения, без слешей.
  6. Наконец, мы требуем, чтобы после нечетного количества слешей был один из управляющих символов [ibud]. Так как у регулярки указан модификатор i, совпадение будет в любом регистре.

Если не использовать жемчужину этой регулярки, (*SKIP), можно сочинить выражение с ретроспективной проверкой (lookbehind): #(?<=^|[^\\])\\(?:\\\\)*\K[ibud]#i. Правда, оно будет менее эффективно на строках с обратным слешем. Ну а наивное выражение #(?:^|[^\\])\\(?:\\\\)*\K[ibud]#i будет медленнее на любых строках, так как не начинается с фиксированного символа обратного слеша.

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

preg_match_all('#\\\\(?:\\\\(*SKIP)\\\\)*\K[ibud]#i', $text, $matches);

foreach ($matches[0] as $match) {
    // в $match будет один из символов ibudIBUD
}
    Оставить комментарий

Codeium — нейросетевой помощник программиста

2 апреля 2024 года, 00:07

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

Как работает Codeium

Я установил его как плагин к PhpStorm. Для работы он требует войти в аккаунт, но регистрация бесплатна.

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

После нажатия на tab и перевода строки нейросеть продолжает сочинять. Вот тут одним махом предлагает написать весь оставшийся код метода:

Нейросеть «поняла» из названия метода, что мне нужна версия алгоритма для вычисления расстояния Левенштейна между строками, корректно работающая с кодировкой UTF-8 (в PHP есть встроенная функция levenshtein(), но она правильно работает только для однобайтных кодировок). Идея алгоритма оказалась правильной, но с деталями не вышло: сравнение $a[$i - 1] === $b[$j - 1] берет не символы с соответствующими номерами, а байты. После исправления этого фрагмента на mb_substr($a, $i - 1, 1) === mb_substr($b, $j - 1, 1) код заработал правильно.

Второй способ взаимодействия — это чат. Он мне показался туповатым по сравнению с ChatGPT. Я так и не понял, лучше ли работают английские запросы, или можно писать по-русски. На скриншотах выше видны шорткаты для быстрой формулировки запросов в чате: Refactor, Explain, Docstring. Explain описывает на «человеческом» языке, что происходит в коде. Возможно, пригодится в командной работе, когда программист в основном разбирается в чужом коде, а не пишет свой. Я же пробовал Codeium на своем коде, о котором я и так всё знаю, так что описание мне не пригодилось.

Главный вау-эффект

Наибольшая помощь от «искусственного интеллекта» была в переписывании кода и конфигурации из одного формата в другой. Допустим, вы меняете формат какой-нибудь конфигурации из XML в JSON или заменяете множество вызовов одного API аналогичными вызовами другого. Из классических инструментов может помочь замена с помощью регулярных выражений или мультикурсор, да и то не всегда. В примере на скриншоте ниже поможет только ручное копирование каждого значения и ввод служебных символов. А с Codeium я скопировал старый код в новый файл, начал дописывать новый код, и он, распознав закономерность, стал предлагать в автодополнении правильное продолжение на основе старого кода.

Недостатки

Теперь о недостатках, куда же без них. Во-первых, Codeium может только добавлять код. Он не может предложить удалить какой-то фрагмент. Во-вторых, он подтормаживает. Тормозит и появление фрагмента для автодополнения, и последующее редактирование текста. Заметно, что автодополнение сделано не нативно — серый текст располагается не в какой-то выпадайке, а прямо внутри редактируемого текста. И если я не принимаю подсказу, нажимая tab, а продолжаю дописывать текст или передвигаю курсор стрелками, проходит заметное время, прежде чем серый текст удалится и появятся набираемые символы. А курсор при перемещении сначала попадает в серый текст, потом серый текст исчезает и курсор оказывается в нужном месте основного текста. После этого Codeium опять подумает и добавит новую версию серого текста с подсказкой. Даже если плагин не запутается и корректно допишет вводимые символы куда нужно и расположит курсор, такое поведение напрочь убивает мгновенную обратную связь при наборе текста и делает сам набор почти невозможным. Может эти недостатки — следствие ограниченного API плагинов в PhpStorm. Но сейчас всё работает не очень приятно.

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

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

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

Вывод

В процессе работы перевешивают то достоинства нейросетевого помощника, то его недостатки. Забавно наблюдать, как нейросеть «читает» твои мысли, выдавая ровно тот код, который ты сам собрался написать. Правда, происходит такое не всегда. Когда нейросеть предлагает нужные фрагменты кода, их надо тщательно проверить, как за программистом-стажером. В то же время при рефакторинге встроенными в PhpStorm инструментами я отключал Codeium, потому что тогда его подсказки бесполезны, а подтормаживания интерфейса мешают.

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

Да, и не забывайте о вопросах безопасности. Наверняка Codeium отправляет всё редактируемое на свои серверы. Я пробовал его на открытом опубликованном коде своего движка, так что дополнительно ничего «утечь» не может. Если хотите попробовать на работе — проконсультируйтесь с вашим отделом по информационной безопасности.

    Оставить комментарий

Тесты выявляют проблемы не только с вашим кодом

29 марта 2024 года, 00:57

Удивительные тесты

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

<?php
use Codeception\Test\Unit;

class MyTest extends Unit
{
    public function testHash(): void
    {
        $this->assertEquals('098f6bcd4621d373cade4e832627b4f6', md5('test'));
    }
}

Для неспециалистов поясню, что этот тест вызывает встроенную в PHP функцию md5(), передает ей аргумент 'test' и проверяет, что она возвращает указанное значение.

Зачем нужен этот тест, если встроенная функция и так вычисляет хеш по известному задокументированному алгоритму? Мы же пишем тесты на наш проект, а не на интерпретатор PHP. Написавший этот тест коллега на вопросы отвечает так:
— В наших алгоритмах мы полагаемся на значения хешей, сохраненные в базе данных. Если вдруг функция начнет возвращать другие значения в будущих версиях PHP, мы заметим это по упавшему тесту. Конечно, такие изменения нарушают обратную совместимость, и они должны быть написаны в информации о релизе PHP. Но их можно просмотреть из-за человеческого фактора, а упавший тест заставит разбираться, в чем проблема. Остальные тесты используют логику уникальности значения хеша, но само значение не проверяют. Поэтому пришлось написать вот такой тест.

Как считаете, писать в проекте тест на встроенную функцию PHP — это паранойя? Или разумная предусмотрительность? А что, если это не встроенная функция в PHP, а сторонняя библиотека? Правда же, такой тест вызывает меньше удивления:

<?php
use Codeception\Test\Unit;
use SuperVendor\SuperHashLib\SuperHash;

class MyTest extends Unit
{
    public function testHash(): void
    {
        $this->assertEquals('098f6bcd4621d373cade4e832627b4f6', SuperHash::getHash('test'));
    }
}

Тест выявил изменение поведения при обновлении PHP

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

<?php

class A
{
    public $prop1 = '1';
    public $prop2 = '2';

    public function getHash(): string
    {
        return md5(serialize($this->normalize()));
    }

    public function normalize(): array
    {
        return get_object_vars($this);
    }
}

class B extends A
{
    public $prop3 = '3';
}

$a = new A;
var_dump($a->getHash());
$b = new B;
var_dump($b->getHash());

Этот код в старых версиях PHP до 8.1 выводит следущее:

string(32) "e5f8d9c52536e3412aa273c7bd4c9dbb"
string(32) "46d9b1133eec8d47fe6e00e970cf0a77"

А начиная с 8.1 значение хеша у объекта класса B изменилось:

string(32) "e5f8d9c52536e3412aa273c7bd4c9dbb"
string(32) "8275b764cf277cbbd3b00b1e86d8a4eb"

Причина различий в изменении порядка свойств в массиве, возвращаемом get_object_vars(). В старых версиях сначала шли свойства самого класса, а затем унаследованные от родительского:

// До PHP 8.1
Array
(
    [prop3] => 3
    [prop1] => 1
    [prop2] => 2
)

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

// PHP 8.1 и старше
Array
(
    [prop1] => 1
    [prop2] => 2
    [prop3] => 3
)

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

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

<?php

class A
{
    public $prop1 = '1';
    public $prop2 = '2';
    public $propN = null;

    public function getHash(): string
    {
        return md5(serialize($this->normalize()));
    }

    private function normalize(): array
    {
        $data = get_object_vars($this);
        if ($this->propN === null) {
            unset($data['propN']);
        }
        return $data;
    }
}

Чтобы разработчики не забывали добавлять unset() новых null-овых полей, мы в одном из тестов зафиксировали точное значение хеша. И этот тест выявил вредный побочный эффект от нарушения обратной совместимости при обновлении PHP.

Пример решения проблем с обратной совместимостью алгоритмов хеширования

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

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

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

idhashdata
1001  e5f8d9c52…{"color":"red"…}
10028275b764c…{"color":"green"…}

После этого для записи истории цен достаточно вести таблицу price_history:

product_id  dateprice
10012024-03-28  1099,9
10012024-03-291199,9

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

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

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

    public function getHash2(): string
    {
        $data = $this->normalize();
        ksort($data);
	    
        return md5(serialize($data));
    }

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

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

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

Вывод

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

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

    Оставить комментарий

туда →

Поделиться
Записи

Подписка на RSS (?)