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

Первая нормальная форма: в таблицах есть первичные ключи, отсутствуют дубли, к тому же нет составных данных, то есть не допускается хранение данных в одной записи в виде массива или просто утрамбованных в текстовый тип данных с разделителем. Про то, как я мучался с дублями в таблице в ~500Гб можно почитать тут. А еще у меня на одном из рабочих проектов был опыт, когда одно из хранимых мной полей могло быть long, long[], guid или objectId (монговский формат "уникального" id). Сохранение в виде строки не проходило по требованиям к объему базы, раскидывание по разным строкам - по требованиям к производительности. В итоге я изобразил свой бинарный формат и хранил тупо байтики.

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

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

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

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

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

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

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

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

Изначально все вызовы я пустил строго последовательно, рассудив, что отладка параллельных вызовов превратится в ад (так в последствии и оказалось).

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

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

#архитектура
Распишу один из самых часто задаваемых вопросов на собеседованиях - принципы SOLID.

S: Single Responsibility Principle (Принцип единственной ответственности). Класс должен решать только какую-то одну задачу. Видел в интернетах много высосанных из пальца примеров, попробую дать примеры из жизни. К основному нарушению этого принципа я бы отнес замешивание в одном месте бизнесового и инфраструктурного кода. Имеем класс с методом с какой-то ядреной бизнес логикой, и тут, ВНЕЗАПНО нам нужно получить дополнительные данные. Не выходя из метода открываем подключение к БД, формируем запрос, читаем ответ, продолжаем бизнес логику. Чувствуете запах?:) Или другой вариант - замешиваем в одном классе получение данных из брокера и их бизнесовую обработку.
Ну и классический пример - антипаттерн singleton. Плоха не единственность экземпляра класса, а самоплальная логика обеспечения единственности.

O: Open-Closed Principle (Принцип открытости-закрытости). Классы должны быть открыты для расширения и закрыты для модификации. Самый простой пример, попирающий этот принцип - использование else if бесконечных размеров, куда по мере развития добавляются новые вариации ветвлений со своей логикой. Самое интересное начинается спустя несколько лет, когда приходится спиливать лишние загибы ветвлений или что-то изменять, оставляя часть функционала неизменной. Альтернатива - больше использовать следующий принцип.

L: Liskov Substitution Principle (Принцип подстановки Барбары Лисков). Объекты в программе должны быть заменяемы их наследниками без изменения корректности программы. В целом - самый очевидный принцип, который осознанно нарушают достаточно редко.

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

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

#собес
#solid
Эшу быдлокодит
Распишу один из самых часто задаваемых вопросов на собеседованиях - принципы SOLID. S: Single Responsibility Principle (Принцип единственной ответственности). Класс должен решать только какую-то одну задачу. Видел в интернетах много высосанных из пальца…
По горячим следам пример к букве I - разделению интерфейсов.

Например: пишем бота в телеграмме, у нас есть сущность - сообщение. Хочется (если честно - не очень) сделать какой-то интерфейс для типичных методов для работы с ним IMessage с методами GetText, GetAttachedMedia, LogToDB и т.д.

Но у нас есть нюанс: сообщения бывают входящие и исходящие. Исходящие после формирования надо отправлять в телегу, то есть нужен метод Send.

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

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

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

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

Начали выполнение - добавили запись в БД. Следущий шаг - ещё запись, в другое место.

Если все шаги выполнены успешно - коммитнули транзакцию.

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

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

#собес
#solid
А вообще, мы в отпуске. Стартовали вчера в составе жена, я, два ребенка - 6 и 3.5 лет и племянница 14 лет. Мы поехали на машине в Мурманск и окрестности. Первый перегон - Москва - Волхов. Заночевали в посуточно сдающейся квартире. Несмотря на неказистый вид дома позапрошлого века постройки, для квартиры за 2тыс/сутки прям отлично. И детям громить особенно нечего:)

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

#отпуск
Перейдем к ситуациях, когда допустимо попирать следущую букву.

O. Принцип открытости-закрытости: классы должны быть открыты для расширения, но закрыты для модификации.

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

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

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

#собес
#solid
Перейду к описанию кейсов, когда нарушение принципа, соответствующего следующей букве в аббревиатуре #solid оправдано.

L. Принцип подстановки Барбары Лисков: объекты в программе должны быть заменяемы их наследниками без изменения корректности программы.

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

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

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

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

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

#собес
Сразу после пробуждения упаковались, посмотрели на миниатюрную Волховскую ГЭС и отправились в Карелию. Десять лет назад мы уже были тут, это было наше первое автомобильное путешествие.

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

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

Выложу видео сямозерского прибоя и немного зверей из зоокомплекса.

А теперь движемся дальше: север ждёт!

#отпуск
Пришел через нарушить следущий из принципов #solid - буковку I - Interface segregation - принцип разделения интерфейсов, который я иллюстрировал вышел.

Рассмотрим ситуацию: мы - банк. У нас много денег и высока цена ошибки. Айти огромное, а системы крайне сложны.

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

Итого на выходе мы имеем шаблон, в котором предусмотрено единообразным способом:
1. Логирование ошибок и основных этапов, система метрик, трассировка используемых запросов.
2. Простой механизм использования кэша, скрывающий особенности реализации: методы Put и Get
3. Механизм для проведения и отката распределенной транзакции.
4. Механизм увязывания операций в цепочки.
5. Механизм авторизации и аутентификации.

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

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

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

#собес
Сегодня совершили переезд из Петрозаводска за полярный круг, в окрестности Кандалакши.

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

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

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

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

#отпуск
This media is not supported in your browser
VIEW IN TELEGRAM
Оставив расцветающую в Кандалакше сирень позади, мы продолжили наш путь на север. В какой-то момент дети попросили размять ноги и поискать грибы, ну мы и съехали на берег реки Пиренги.

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

#отпуск
This media is not supported in your browser
VIEW IN TELEGRAM
Следующей нашей остановкой стал город металлургов Мончегорск. Земля вокруг города полумертвая от деятельности человека, ни травы, ни мха, ни лишайников, только кусты вместо обычной растительности лесотундры. Зато в городе очень вкусная столовка "Пышка" и есть смотровая площадка на горке на высоте 360 метров, куда можно заехать на машине.

Вид с горы потрясающий: невысокий хвойный лес на холмах, гладь озер, маленькие коробочки домиков. Но погода была лютая: +4 и ветер от 9 м/с с более сильными порывами, осыпающий полузамерзшими каплями дождя. Сняв пару видео мы спрятались в теплую машину и покатили дальше в сторону Териберки.

P.S. здесь и далее периодически будет реклама автомобиля УАЗ "Патриот" :)

#отпуск
2025/07/08 02:29:10
Back to Top
HTML Embed Code: