<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <title>Tune IT</title>
  <link rel="self" href="" />
  <subtitle>Tune IT</subtitle>
  <id />
  <updated>2026-04-21T02:38:29Z</updated>
  <dc:date>2026-04-21T02:38:29Z</dc:date>
  <entry>
    <title>Анатомия письма: почему email-дизайн — это машина времени в 1999 год</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21239713" />
    <author>
      <name>Алексей Кондратьев</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21239713</id>
    <updated>2026-04-08T17:34:56Z</updated>
    <published>2026-04-08T16:59:00Z</published>
    <summary type="html">&lt;p&gt;Вы когда-нибудь пробовали применить display: flex или grid в коде письма? Если да, то вы знаете чувство глубокого разочарования, когда Gmail, Outlook или Yahoo решают, что ваш современный красивый макет должен выглядеть как стена текста, набранная на печатной машинке.&lt;/p&gt;

&lt;p&gt;Добро пожаловать в мир email-дизайна — уникальной дисциплины, где правила веб-разработки не работают, а стандарты де-факто застыли в эпохе Netscape Navigator. Здесь дизайнер и верстальщик вынуждены использовать табличную вёрстку, писать инлайновые стили и воевать с условными комментариями для Outlook. И тем не менее, именно здесь рождаются письма, которые читают, по которым кликают и которые приносят миллионы.&lt;/p&gt;

&lt;p&gt;Эта статья — глубокое погружение в специфику создания адаптивных писем. Мы разберем, почему таблицы до сих пор правят бал, как сделать письмо красивым на iPhone и при этом не сломать его на древней версии Microsoft Outlook.&lt;br /&gt;
&lt;br /&gt;
&lt;b id="docs-internal-guid-849f4ac7-7fff-726e-277c-e88d7b336db7"&gt;Часть 1. Великий парадокс: Почему email живет в прошлом?&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;В отличие от сайтов, которые вы открываете в одном-двух браузерах (Chrome, Safari), email-клиентов — сотни. Каждый из них использует свой движок для рендеринга HTML:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Gmail (веб и приложение) использует свой собственный, сильно урезанный HTML-парсер.&lt;/li&gt;
	&lt;li&gt;Outlook (десктоп) печально известен тем, что использует движок Microsoft Word (!) для отображения HTML.&lt;/li&gt;
	&lt;li&gt;Apple Mail — один из самых «продвинутых», поддерживает современные CSS.&lt;/li&gt;
	&lt;li&gt;Яндекс.Почта, Mail.ru — имеют свои особенности.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Чтобы письмо выглядело одинаково (или хотя бы читаемо) во всех этих средах, разработчики вынуждены опираться на наименьший общий знаменатель — технологии, которые были стандартом 20 лет назад.&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-8c2f7877-7fff-2589-2a3e-92bf3248c5fe"&gt;Часть 2. Табличная вёрстка: Скелет вашего письма&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Забудьте про &amp;lt;div&amp;gt; с display: flex. В мире email главный строительный блок — это &amp;lt;table&amp;gt;.&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-b64a5411-7fff-7c83-f27f-6dd3592ce2ce"&gt;2.1. Почему таблицы?&lt;/b&gt;&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Предсказуемость: Таблицы были созданы для отображения структурированных данных. Их алгоритм рендеринга (как ячейки растягиваются и выравниваются) одинаков во всех клиентах.&lt;/li&gt;
	&lt;li&gt;Надёжность: Они устойчивы к «съеданию» тегов и стилей. Если какой-то CSS не сработает, таблица всё равно останется таблицей.&lt;/li&gt;
	&lt;li&gt;Вложенность: Вся вёрстка письма — это матрёшка из вложенных друг в друга таблиц.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-aa6e7488-7fff-1746-1494-0ba87bfbe014"&gt;2.2. Базовая структура «резинового» письма&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Современное адаптивное письмо строится по следующему шаблону:&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
&amp;lt;center&amp;gt;
    &amp;lt;table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0"&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td align="center"&amp;gt;
                &amp;lt;!-- Основной контейнер письма (ширина 600px) --&amp;gt;
                &amp;lt;table role="presentation" width="600" border="0" cellpadding="0" cellspacing="0"&amp;gt;
                    &amp;lt;!-- ШАПКА --&amp;gt;
                    &amp;lt;tr&amp;gt;
                        &amp;lt;td&amp;gt; ... &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                    &amp;lt;!-- ГЕРОЙ (Баннер) --&amp;gt;
                    &amp;lt;tr&amp;gt;
                        &amp;lt;td&amp;gt; ... &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                    &amp;lt;!-- ТЕЛО С КОЛОНКАМИ --&amp;gt;
                    &amp;lt;tr&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0"&amp;gt;
                                &amp;lt;tr&amp;gt;
                                    &amp;lt;td class="stack" width="280"&amp;gt; Левая колонка &amp;lt;/td&amp;gt;
                                    &amp;lt;td class="stack" width="20"&amp;gt; Отступ &amp;lt;/td&amp;gt;
                                    &amp;lt;td class="stack" width="280"&amp;gt; Правая колонка &amp;lt;/td&amp;gt;
                                &amp;lt;/tr&amp;gt;
                            &amp;lt;/table&amp;gt;
                        &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                    &amp;lt;!-- ФУТЕР --&amp;gt;
                    &amp;lt;tr&amp;gt;
                        &amp;lt;td&amp;gt; ... &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                &amp;lt;/table&amp;gt;
            &amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/table&amp;gt;
&amp;lt;/center&amp;gt;&lt;/pre&gt;

&lt;p&gt;Ключевые моменты:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;role="presentation" — говорит скрин-ридерам, что это таблица для вёрстки, а не для данных.&lt;/li&gt;
	&lt;li&gt;width="100%" — делает внешнюю таблицу «резиновой».&lt;/li&gt;
	&lt;li&gt;align="center" — центрирует всё письмо.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-aedc18bc-7fff-8923-d233-76ceb55a2c1b"&gt;Часть 3. Адаптивность: Как превратить две колонки в одну&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;На десктопе мы видим две колонки текста. На мобильном телефоне они должны встать друг под друга. Это достигается с помощью медиа-запросов и трюка с display: block !important.&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-b3c933fd-7fff-ac7d-9e78-25391708c8eb"&gt;3.1. Медиа-запросы&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Мы задаём правило: если ширина экрана меньше 600px, то заставляем наши колонки (с классом .stack) стать блочными и растянуться на 100%.&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
@media only screen and (max-width: 600px) {
    .stack {
        display: block !important;
        width: 100% !important;
    }
}&lt;/pre&gt;

&lt;p&gt;В нашем примере выше, у колонок ширина 280px и отступа 20px. На мобильном они получат width: 100% и выстроятся вертикально, а отступ превратится в пустую строку.&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-cceb5875-7fff-3d3c-4796-15b1d299b2d0"&gt;3.2. Проблема кнопок&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Кнопки в письмах — это главная головная боль. &amp;lt;button&amp;gt; работает плохо. Ссылка &amp;lt;a&amp;gt; с padding тоже часто ломается в Outlook.&lt;br /&gt;
Надёжный способ (Bulletproof Button):&lt;br /&gt;
Сделать кнопку через границы ячейки таблицы.&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
&amp;lt;table role="presentation" border="0" cellpadding="0" cellspacing="0"&amp;gt;
    &amp;lt;tr&amp;gt;
        &amp;lt;td align="center" bgcolor="#007bff" style="border-radius: 4px;"&amp;gt;
            &amp;lt;a href="https://example.com" style="display: inline-block; padding: 12px 24px; color: #fff; text-decoration: none; font-weight: bold;"&amp;gt;Купить сейчас&amp;lt;/a&amp;gt;
        &amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/pre&gt;

&lt;p&gt;Почему это работает? Фон задаётся ячейке (td), а ссылка внутри просто растягивается. Outlook не любит фон у ссылок, но любит фон у ячеек.&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-3472f228-7fff-cbf0-2b26-414df624830e"&gt;Часть 4. Тонкая настройка: Инлайновые стили и условные комментарии&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-aab3cb20-7fff-e212-56af-67df1a394009"&gt;4.1. Инлайн — король&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Никогда не полагайтесь на то, что стили, прописанные в &amp;lt;head&amp;gt; или внешнем CSS, сработают. Gmail, например, вырезает &amp;lt;style&amp;gt; при определённых условиях. Правило: все стили, касающиеся отступов, цветов, границ, шрифтов, должны быть прописаны в атрибуте style каждого тега.&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
&amp;lt;td style="padding: 20px; font-family: Arial, sans-serif; color: #333;"&amp;gt;
&lt;/pre&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-6271f925-7fff-5c98-5d53-9a59c8930c7c"&gt;4.2. Условные комментарии для Microsoft Outlook&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Outlook (особенно 2007, 2010, 2013) — это темная лошадка. Чтобы задать стили только для Outlook, используют условные комментарии, которые никто, кроме Outlook, не видит.&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
&amp;lt;!--[if mso]&amp;gt;
    &amp;lt;style type="text/css"&amp;gt;
        /* Стили, которые увидит только Outlook */
        .outlook-button { background: #000; }
    &amp;lt;/style&amp;gt;
&amp;lt;![endif]--&amp;gt;
&lt;/pre&gt;

&lt;p&gt;А для обратной ситуации (спрятать что-то от Outlook):&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
&amp;lt;!--[if !mso]&amp;gt;&amp;lt;!--&amp;gt;
    &amp;lt;div style="display: none;"&amp;gt;Это увидят все, кроме Outlook&amp;lt;/div&amp;gt;
&amp;lt;!--&amp;lt;![endif]--&amp;gt;
&lt;/pre&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-e8cd7281-7fff-49e5-a3d0-c7ece27c491c"&gt;Часть 5. Изображения: Смертельный номер&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;В email-дизайне изображения — это источник проблем. Многие почтовые клиенты по умолчанию блокируют загрузку картинок (пользователь должен нажать «Показать изображения»). Если ваше письмо — одна большая картинка, пользователь увидит пустоту.&lt;/p&gt;

&lt;p&gt;Правила работы с изображениями:&lt;/p&gt;

&lt;ol&gt;
	&lt;li&gt;Не кладите текст на картинки. Если картинка не загрузится, текст не увидят.&lt;/li&gt;
	&lt;li&gt;Всегда прописывайте ALT-текст. И делайте его полезным: не «логотип», а «Купите iPhone со скидкой 20%».&lt;/li&gt;
	&lt;li&gt;Используйте атрибуты ширины и высоты. Без них Outlook может отобразить картинку размером 1x1 пиксель.&lt;/li&gt;
	&lt;li&gt;
	&lt;pre class="brush:xml;"&gt;
&amp;lt;img src="..." alt="Описание" width="600" height="200" style="display: block; width: 100%; height: auto;"&amp;gt;&lt;/pre&gt;
	&lt;/li&gt;
	&lt;li&gt;Спрайты и адаптивность: Чтобы картинка сжималась на мобильном, дайте ей style="width: 100%; height: auto;".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-cf785bc7-7fff-9604-c2d8-aea609b58089"&gt;Часть 6. Инструменты и тестирование&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Спроектировать письмо «на глаз» невозможно. Вы обязательно что-то сломаете в Outlook или Gmail.&lt;/p&gt;

&lt;p&gt;Современный подход к созданию писем:&lt;/p&gt;

&lt;p&gt;Дизайн в Figma: Рисуйте макет, помня, что ширина письма редко превышает 600px (оптимально для чтения).&lt;/p&gt;

&lt;p&gt;Фреймворк MJML: Это спасение. Вы пишете простой, понятный код на MJML, а компилятор превращает его в идеально работающую табличную вёрстку. Это стандарт индустрии.&lt;/p&gt;

&lt;p&gt;Тестирование (важно!):&amp;nbsp;&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Litmus / Email on Acid: Платные сервисы, которые прогоняют ваше письмо через 90+ клиентов и показывают скриншоты.&lt;/li&gt;
	&lt;li&gt;PutsMail: Бесплатный инструмент для отправки тестовых писем.&lt;/li&gt;
	&lt;li&gt;Реальная отправка: Заведите себе аккаунты в Gmail, Яндекс.Почте, Outlook.com, Mail.ru и отправляйте письма себе.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-6b8e47b0-7fff-248e-3230-749e3dda26f3"&gt;Заключение: Это не баг, это фича&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Дизайн для email-рассылок раздражает веб-разработчиков своей архаичностью. Но это осознанный выбор, продиктованный средой. Умение создать надёжное, красивое и конвертирующее письмо, которое работает даже в Outlook 2007, — это признак высокой квалификации.&lt;/p&gt;

&lt;p&gt;В следующий раз, когда вы откроете красивую рассылку от Apple или Amazon, знайте: внутри неё — десятки вложенных таблиц, условные комментарии и надежда, что пользователь включил отображение картинок. Это и есть магия email-дизайна.&lt;br /&gt;
&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;
&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Алексей Кондратьев</dc:creator>
    <dc:date>2026-04-08T16:59:00Z</dc:date>
  </entry>
  <entry>
    <title>Нативная поддержка gRPC в Spring</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21239144" />
    <author>
      <name>Maxim Kalabukhov</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21239144</id>
    <updated>2026-04-07T16:08:06Z</updated>
    <published>2026-04-07T15:04:00Z</published>
    <summary type="html">&lt;p&gt;Не так давно в релиз был выкачена первая мажорная версия библиотеки&amp;nbsp;Spring gRPC -&amp;nbsp;для встроенной поддержки gRPC-сервисов в приложении Spring Boot.&amp;nbsp;Она развивается внутри экосистемы Spring, а значит версионирование и интеграция с Boot - это теперь не вызывает проблем. У меня появился интерес протестить это in action.&lt;br /&gt;
&lt;br /&gt;
​​​​Начнем с того, что подключим необходимые зависимости в &lt;code&gt;build.gradle.kts&lt;/code&gt;:&lt;/p&gt;

&lt;div class="portlet-msg-info"&gt;implementation("org.springframework.grpc:spring-grpc-spring-boot-starter")&lt;br /&gt;
implementation("com.google.protobuf:protobuf-java")&lt;/div&gt;

&lt;p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"&gt;Допустим, у нас есть такой proto-файл:&lt;br /&gt;
​​​​​&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}&lt;/pre&gt;

&lt;p&gt;Реализация сервиса на kotlin будет выглядить следующим образом:&lt;br type="_moz" /&gt;
&amp;nbsp;&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
@GrpcService 
class GreeterService : GreeterGrpc.GreeterImplBase() {

    override fun sayHello(
        request: HelloRequest,
        responseObserver: StreamObserver&amp;lt;HelloReply&amp;gt;
    ) {
        val reply = HelloReply.newBuilder()
            .setMessage("Hello, ${request.name}!")
            .build()

        responseObserver.onNext(reply)
        responseObserver.onCompleted()
    }
}
&lt;cite&gt;​​​​​​​// можно использовать и аннотацию &lt;code&gt;@Service&lt;/code&gt;, но на личный вкус &lt;code&gt;@GrpcService&lt;/code&gt; выглядит нагляднее&lt;/cite&gt;&lt;/pre&gt;

&lt;p&gt;Spring gRPC автоматически обнаружит все бины, которые реализуют &lt;code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]"&gt;BindableService&lt;/code&gt; (это интерфейс, от которого наследует &lt;code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]"&gt;ImplBase&lt;/code&gt;), и зарегистрирует их на gRPC-сервере.&lt;br /&gt;
​​​​&lt;/p&gt;

&lt;p&gt;Дело за малым - осталось всё собрать и протестировать. Делать я это буду с помощью cli тулзы grpcurl, которая как раз предназначена для протыкивания grpc сервисов.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;
&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21214588/grpc+run.png/9e7b3773-5c0e-5a92-93e9-35f18c03c74e?imagePreview=1" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21214588/grpcresult.png/d3be2251-399d-8fb6-6b93-18d848a58612?imagePreview=1" /&gt;&lt;/p&gt;

&lt;p class="svelte-121hp7c" dir="auto"&gt;Подводя итог, можно сказать, что Spring gRPC успешно решает главную задачу - делает интеграцию gRPC в Spring Boot максимально бесшовной и нативной. Как показал тест, весь процесс настройки сводится к подключению стартера и созданию класса с аннотацией &lt;code class="cursor-pointer codespan"&gt;@Service&lt;/code&gt;.&amp;nbsp;Автоматическое обнаружение сервисов и привычная работа через DI позволяют сосредоточиться на бизнес-логике, не отвлекаясь на инфраструктурные сложности.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;​​​​​​​​​​​​​​&lt;/p&gt;</summary>
    <dc:creator>Maxim Kalabukhov</dc:creator>
    <dc:date>2026-04-07T15:04:00Z</dc:date>
  </entry>
  <entry>
    <title>Кэширование пользовательских ролей из стороннего сервиса при Stateless-аутентификации (Spring)</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21238639" />
    <author>
      <name>Никита Рогаленко</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21238639</id>
    <updated>2026-04-06T13:47:44Z</updated>
    <published>2026-04-06T13:45:00Z</published>
    <summary type="html">&lt;p style="text-align: justify;"&gt;Представим не столь редкую ситуацию, когда у нас есть некоторый сервис на базе Spring, к которому шлет запросы клиентское приложение. Фронтэнд взаимодействует с Keycloak (или другим сервером аутентификации, не столь важно), определяет пользователя, делающего запросы, и при обращении к нашему сервису подкладывает в заголовок "Authorization" Bearer-токен в виде JWT, на основании которого в сервисе должны происходить идентификация пользователя и определение прав доступа.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Схема обычная и соответствует стандарту авторизации OAuth 2.0, однако проблемы возникают, если нам необходимо непосредственно в процессе авторизации выдать те или иные права, основываясь на данных из другого сервиса, либо в целом приходится использовать какие-либо данные извне. Несмотря на все преимущества stateless-аутентификации, среди которых масштабируемость и производительность, в данном случае она будет проблемой, потому что при данном подходе сервер не использует сессии и не хранит никакой информации о клиенте между запросами. Таким образом, информация из стороннего сервиса будет запрашиваться заново при каждом запросе, что может сильно ударить по производительности системы. Конечно подобные проблемы лучше решать еще на этапе проектирования архитектуры всей системы, но если ситуация уже возникла, то решение есть.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Чтобы не запрашивать данные из стороннего сервиса каждый раз, можно прибегнуть к кэшированию данных, сохраняя их по идентификатору пользователя. Для этого есть несколько решений, но мы рассмотрим вариант, оптимальный&amp;nbsp; по сложности реализации и скорости выполнения. Это будет стандартная поддержка кэширования Spring и библиотека&amp;nbsp;Caffeine, которая де-факто уже также стала частью стандарта кэширования в Spring. Это будет быстрее, чем читать и обновлять значения из Redis, поднимать который имеет смысл лишь в случае множества распределенных сервисов, поскольку в данном случае кэш будет храниться прямо в памяти приложения.&amp;nbsp;В дополнение к этому мы также напишем конвертер для аутентификации, который при входе будет объединять роли из Keycloak и роли, приходящие из стороннего сервиса, чтобы в нашем сервисе можно было единообразно их использовать.&amp;nbsp;&amp;nbsp;&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Шаг 1. Зависимости и конфигурация приложения&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
	&lt;li style="text-align: justify;"&gt;Добавить зависимости:&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="brush:as3;"&gt;
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'com.github.ben-manes.caffeine:caffeine'
}&lt;/pre&gt;

&lt;ul&gt;
	&lt;li style="text-align: justify;"&gt;В главный класс надо добавить аннотацию&amp;nbsp;@EnableCaching, которая включит кэширвание в Spring&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;Добавить в application.yml нужные настройки:&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="brush:as3;"&gt;
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://your-keycloak.com/realms/test
  cache:
    cache-names: infoFromOtherService
    caffeine:
      spec: expireAfterWrite=60s,maximumSize=1000&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;Здесь под issuer-uri мы указываем адрес Keycloak для проверки валидности токена, который пришел с клиентского приложения. В разделе cache указываем название для кэша, где мы будем хранить информацию о пользователе, приходящую из стороннего сервиса. В spec мы указываем настройки caffeine, которые помогут не очищать кэш самостоятельно, а задать ему срок жизни (в данном случае 60 секунд), после которого данные будут запрашиваться снова. В maximumSize пишется максимальный размер кэша, чтобы он не мог занять слишком много места.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Шаг 2. Сервис, который запрашивает данные о пользователе из стороннего сервиса&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;

&lt;pre class="brush:java;"&gt;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

​​​​​​​@Service
public class ExternalSourceService {

    // sync = true защищает от повторной отправки застрявших в очереди запросов при истечении кэша
    @Cacheable(value = "infoFromOtherService", key = "#username", sync = true)
    public List&amp;lt;String&amp;gt; getInfoFromExternalSource(String username) {
        // Вызов API
        return externalSourceClient.findExternalInfoByUsername(username); 
    }
}&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;Это самый обычный сервис, который делает необходимый запрос по указанному API. Не будем приводить его реализацию, поскольку это сильно зависит от конкретного случая, отметим только необходимость добавить аннотацию&amp;nbsp;Cacheable, value которой совпадает с названием кэша из настроек. С помощью этой аннотации мы отмечаем, что результаты именно этого метода необходимо кэшировать.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Шаг 3. Конвертер, преобразующий JWT-токен&lt;/strong&gt;​​​​​​​&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
​​​​​​​
@Component
@RequiredArgsConstructor
public class AuthenticationConverter implements Converter&amp;lt;Jwt, AbstractAuthenticationToken&amp;gt; {

    @Autowired
    private final ExternalSourceService externalSourceService;

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        UUID userId = UUID.fromString(jwt.getSubject()); // UUID пользователя
        String username = jwt.getClaimAsString("preferred_username"); // имя пользователя
        Collection&amp;lt;GrantedAuthority&amp;gt; roles = extractKeycloakRoles(jwt); // получаем роли из Keycloak
&amp;nbsp;       // получаем информацию о пользователе из стороннего источника (что кэшируется)
        String externalSourceUserInfo = externalSourceService.getInfoFromExternalSource(username);
&amp;nbsp;       try {
            // пытаемся добавить роли пользователя из стороннего источника
            Collection&amp;lt;GrantedAuthority&amp;gt; rolesFromExternalSource = extractExternalRoles(externalSourceUserInfo);
            roles.addAll(rolesFromExternalSource);
        } catch (JsonProcessingException e) {
            log.error("Cannot parse user info ", e);
        }
        return new JwtAuthenticationToken(jwt, roles, username);
    }

    private Collection&amp;lt;GrantedAuthority&amp;gt; extractKeycloakRoles(Jwt jwt) {
        Map&amp;lt;String, Object&amp;gt; realmAccess = jwt.getClaim("realm_access");
        if (realmAccess == null || realmAccess.isEmpty()) return Collections.emptyList();
        Collection&amp;lt;String&amp;gt; roles = (Collection&amp;lt;String&amp;gt;) realmAccess.get("roles");
        return roles.stream()
                .map(role -&amp;gt; new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
    }

    private Collection&amp;lt;GrantedAuthority&amp;gt; extractExternalRoles(String externalSourceUserInfo) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode rootNode = objectMapper.readTree(externalSourceUserInfo);
​​​​​​​        // достаем нужный кусок JSON, но парсинг ответа от стороннего сервиса зависит от особенностей конкретной системы
        JsonNode globalRoles = rootNode.get("roles");
        return objectMapper.convertValue(globalRoles, new TypeReference&amp;lt;List&amp;lt;String&amp;gt;&amp;gt;() {
                }).stream().map(role -&amp;gt; new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
    }&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Шаг 4. Конфигурация Spring Security&lt;/strong&gt;&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationConverter authenticationConverter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                    .antMatchers("/api/v1/example/**").authenticated()
                    .anyRequest().hasAnyRole("EXTERNAL_SOURCE_ADMIN")
                .and().oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);

    }
}&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;Здесь мы уже можем в правилах проверки прав использовать те роли, которые мы получили из стороннего сервиса (вместе с кейклоковскими). И главное, что получение этих ролей не будет отрабатывать при каждом запросе к нашему сервису, а будет происходить лишь по истечении кэша, настройки которого, с использованием Caffeine, можно регулировать в application.yml.&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Никита Рогаленко</dc:creator>
    <dc:date>2026-04-06T13:45:00Z</dc:date>
  </entry>
  <entry>
    <title>REST API: 5 паттернов формирования ответов, которые используют опытные разработчики</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21236519" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21236519</id>
    <updated>2026-04-01T10:44:07Z</updated>
    <published>2026-04-01T09:28:00Z</published>
    <summary type="html">&lt;style type="text/css"&gt;

article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}

article img {
width: 90%;
}

article li {
 font-size:11pt;   
}

.centered {
text-align:center;
}


article .portlet-msg-info {
color: #232323;
background-color: #f9f9f9;
border-style: dashed;
border-color: #232323;
}

&lt;/style&gt;
&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Введение&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Представьте: вы только что запустили новый сервис. Первые эндпоинты работают, данные приходят, фронтенд доволен. Но спустя полгода приходит задача — переименовать колонку в базе данных. Вы меняете одно поле, и внезапно ломаются три мобильных клиента, два фронтенд-приложения и интеграция с партнёром. Почему? Потому что ваш API с самого начала возвращал сырые сущности напрямую из базы данных.&lt;/p&gt;

&lt;p&gt;Это не гипотетический сценарий. Это ежедневная реальность команд, которые не уделили должного внимания слою формирования ответов.&lt;/p&gt;

&lt;p&gt;Возвращать необработанные JPA-сущности из REST-контроллеров — это один из тех паттернов, который кажется безобидным в начале, но&amp;nbsp;затем превращается в системную проблему в production. Он не просто нарушает принцип разделения ответственности — он создаёт&amp;nbsp;три взаимосвязанных риска одновременно: утечку чувствительных данных, жёсткую связанность API со схемой БД и потерю контроля над публичным контрактом.&lt;/p&gt;

&lt;p&gt;В этой статье мы разберём пять паттернов, которые используют опытные разработчики. Каждый из них решает конкретную задачу, и вместе они формируют слой ответов, который является одновременно безопасным, производительным и удобным в сопровождении.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Проблема прямого возврата сущностей&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Прежде чем переходить к решениям, важно понять, в чем именно состоит&amp;nbsp;проблема.&lt;/p&gt;

&lt;p&gt;Рассмотрим типичную JPA-сущность:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    // Никогда не должно покидать сервер
    private String passwordHash;

    // Внутренние поля, не нужные клиентам
    private String internalNotes;
    private String resetToken;
    private LocalDateTime resetTokenExpiry;

    // Технические поля аудита
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @Version
    private Long version;
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;А теперь — типичный контроллер новичка:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userRepository.findById(id).orElseThrow();
    // В ответе окажется: passwordHash, internalNotes,
    // resetToken, resetTokenExpiry, version...
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Этот код порождает три серьёзные проблемы.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема 1: Утечка чувствительных данных.&lt;/strong&gt;&amp;nbsp;Поле `passwordHash` попадёт в JSON-ответ. Даже если там не открытый пароль, а хеш — это уже вектор для атак. Поле `resetToken` тоже не должно быть видно никому, кроме системы сброса пароля. Можно добавить `@JsonIgnore` на отдельные поля, но это ненадёжно: забудете об одном поле при добавлении — и утечка неизбежна.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема 2: Жёсткая связанность с базой данных.&lt;/strong&gt;&amp;nbsp;Ваш API-контракт теперь является точным отражением вашей схемы БД. Переименовали `firstName` в `first_name` для соответствия coding style? Поздравляем — вы сломали всех клиентов. Разделили таблицу `users` на `users` и `user_profiles`? Теперь нужно менять не только схему, но и весь публичный API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема 3: Отсутствие контроля над контрактом.&lt;/strong&gt;&amp;nbsp;API должен выражать бизнес-понятия, а не структуру хранилища. Клиент хочет получить `fullName` вместо `firstName` + `lastName`? С сырыми сущностями это невозможно без изменения схемы БД.&lt;/p&gt;

&lt;p&gt;Для решения этих проблем, на помощь приходят проверенные паттерны. Рассмотрим их по порядку.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Паттерн 1: Record-классы для формирования ответа&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Традиционные DTO-классы на Java требуют написания конструкторов, геттеров, сеттеров, `equals`, `hashCode` и `toString`. Это десятки строк шаблонного кода на каждую форму ответа. Даже с Lombok это всё равно дополнительные аннотации и косвенность.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Java Records (доступны с Java 16) дают вам неизменяемый объект передачи данных в несколько строк — без Lombok, без шаблонного кода, без сюрпризов.&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
public record UserResponse(
    Long id,
    String firstName,
    String lastName,
    String email
) {}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Вот и всё. Java автоматически генерирует конструктор, геттеры, `equals`, `hashCode` и `toString`. Объект неизменяем по умолчанию — никаких сеттеров, никаких случайных мутаций.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Полный пример&lt;/strong&gt;&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
// Сервисный слой
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public User findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -&amp;gt; new EntityNotFoundException("User not found: " + id));
    }
}

// Контроллер
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/{id}")
    public UserResponse getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return new UserResponse(
            user.getId(),
            user.getFirstName(),
            user.getLastName(),
            user.getEmail()
        );
    }
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Важные детали&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Вложенные Record-классы&amp;nbsp;хорошо работают для составных ответов:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
public record AddressResponse(
    String street,
    String city,
    String country
) {}

public record UserDetailResponse(
    Long id,
    String firstName,
    String lastName,
    String email,
    AddressResponse address,        // вложенный record
    List&amp;lt;String&amp;gt; roles              // коллекции тоже поддерживаются
) {}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Кастомная сериализация&amp;nbsp;настраивается через стандартные Jackson-аннотации:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
public record UserResponse(
    Long id,
    String firstName,
    String lastName,
    String email,

    @JsonFormat(pattern = "dd.MM.yyyy")
    LocalDate birthDate,

    @JsonProperty("isVerified")
    boolean verified
) {}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Почему опытные разработчики это ценят: &lt;/strong&gt;Record-классы самодокументируемы — любой член команды, открыв файл, немедленно видит полный контракт эндпоинта. Не нужно читать маппер, искать аннотации `@JsonIgnore` или гадать, какие поля попадут в ответ.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Паттерн 2: MapStruct для конвертации без шаблонного кода&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Ручное маппирование — серьёзная проблема при масштабировании. Когда у вас десять сущностей, каждая с двадцатью полями, написание маппинга вручную превращается в рутину, которая к тому же молча ломается. Добавили новое обязательное поле в `UserResponse`, но забыли обновить маппер? Компилятор промолчит, тесты не поймают — и вы получите `null` в production.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;MapStruct генерирует код конвертации на этапе компиляции через annotation processing. Вы объявляете только интерфейс — всё остальное делает библиотека.&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
&amp;lt;!-- pom.xml --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.mapstruct&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mapstruct&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.5.5.Final&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.mapstruct&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mapstruct-processor&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.5.5.Final&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Полный пример&lt;/strong&gt;&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
// Базовый маппер
@Mapper(componentModel = "spring")
public interface UserMapper {

    // Простое маппирование: поля с одинаковыми именами — автоматически
    UserResponse toResponse(User user);

    // Маппирование коллекций — бесплатно
    List&amp;lt;UserResponse&amp;gt; toResponseList(List&amp;lt;User&amp;gt; users);

    // Кастомное маппирование полей с разными именами
    @Mapping(source = "passwordHash", target = "hasPassword",
             qualifiedByName = "passwordToBoolean")
    UserAdminResponse toAdminResponse(User user);

    @Named("passwordToBoolean")
    default boolean mapPassword(String passwordHash) {
        return passwordHash != null &amp;amp;&amp;amp; !passwordHash.isEmpty();
    }

    // Игнорирование поля в целевом объекте
    @Mapping(target = "sensitiveData", ignore = true)
    UserPublicResponse toPublicResponse(User user);

    // Вычисляемые поля
    @Mapping(target = "fullName",
             expression = "java(user.getFirstName() + \" \" + user.getLastName())")
    UserSummaryResponse toSummaryResponse(User user);
}&lt;/pre&gt;

&lt;pre class="brush:java;"&gt;
// Использование в контроллере
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;
    private final UserMapper userMapper;

    @GetMapping("/{id}")
    public UserResponse getUser(@PathVariable Long id) {
        return userMapper.toResponse(userService.findById(id));
    }

    @GetMapping
    public List&amp;lt;UserResponse&amp;gt; getAllUsers() {
        return userMapper.toResponseList(userService.findAll());
    }
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Важные детали&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Что происходит при несовпадении полей:&amp;nbsp;MapStruct по умолчанию выдаёт предупреждение компилятора, если у целевого объекта есть поле, которое не замаппировано. Это можно настроить:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
// Превратить предупреждение в ошибку компиляции — рекомендуется для production
@Mapper(
    componentModel = "spring",
    unmappedTargetPolicy = ReportingPolicy.ERROR  // жёсткий режим
)
public interface UserMapper { ... }&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Маппинг между вложенными объектами&amp;nbsp;работает автоматически, если зарегистрировать вспомогательные маппинги:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
@Mapper(componentModel = "spring", uses = {AddressMapper.class})
public interface UserMapper {
    // Address внутри User будет смаппирован через AddressMapper автоматически
    UserDetailResponse toDetailResponse(User user);
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Почему опытные разработчики это ценят:&amp;nbsp;&lt;/strong&gt;&amp;nbsp;MapStruct — это отраслевой стандарт в крупных компаниях. Один интерфейс заменяет десятки строк ручного маппинга, при этом ошибки обнаруживаются на этапе компиляции, а не в production в три часа ночи.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Паттерн 3: Проекции Spring Data для эндпоинтов чтения&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Типичный сценарий: страница со списком пользователей отображает только имя и email. Но ваш репозиторий загружает из базы данных всю сущность — включая `bio`, `avatarUrl`, `preferences`, `notificationSettings` и ещё пятнадцать полей, которые на этой странице просто не нужны. Это избыточный SELECT, лишний трафик и бесполезная нагрузка на сериализатор.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Spring Data поддерживает проекции — интерфейсы, которые указывают репозиторию: «загрузи только эти поля». На уровне SQL это выражается в `SELECT id, first_name, email FROM users` вместо `SELECT * FROM users`.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Полный пример&lt;/strong&gt;&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
// Объявление проекции — просто интерфейс
public interface UserSummary {
    Long getId();
    String getFirstName();
    String getEmail();
}

// Проекция для вложенных объектов
public interface UserWithAddress {
    Long getId();
    String getEmail();
    AddressSummary getAddress();  // вложенная проекция

    interface AddressSummary {
        String getCity();
        String getCountry();
    }
}&lt;/pre&gt;

&lt;pre class="brush:java;"&gt;
// Репозиторий
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {

    // Возвращает только нужные поля
    List&amp;lt;UserSummary&amp;gt; findAllBy();

    // С фильтрацией
    List&amp;lt;UserSummary&amp;gt; findByActiveTrue();

    // Динамическая проекция — тип выбирается вызывающим кодом
    &amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; findByDepartmentId(Long departmentId, Class&amp;lt;T&amp;gt; type);
}&lt;/pre&gt;

&lt;pre class="brush:java;"&gt;
// Контроллер
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserRepository userRepository;

    @GetMapping
    public List&amp;lt;UserSummary&amp;gt; getUsers() {
        return userRepository.findAllBy();
        // SQL: SELECT u.id, u.first_name, u.email FROM users u
    }

    @GetMapping("/list")
    public List&amp;lt;UserListItem&amp;gt; getUserList() {
        // Динамическая проекция
        return userRepository.findByDepartmentId(1L, UserListItem.class);
    }
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Важные детали&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Существуют два типа проекций, и важно понимать разницу:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closed projection&lt;/strong&gt;&amp;nbsp;— интерфейс, где геттеры точно соответствуют полям сущности. Hibernate оптимизирует SQL: `SELECT id, first_name, email FROM users`. Это обеспечивает максимальную производительность.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open projection&lt;/strong&gt;&amp;nbsp;— интерфейс с аннотацией `@Value` и SpEL-выражениями. Hibernate вынужден загружать всю сущность в память, а потом вычислять поле. Производительность хуже, но гибкость выше.&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
// Closed — оптимальный SQL
public interface UserSummary {
    Long getId();
    String getEmail(); // точное соответствие полям сущности
}

// Open — полная загрузка сущности, потом вычисление

// Расширенная проекция с вычисляемым полем через SpEL
public interface UserSummaryOpen {
    Long getId();
    String getEmail();
    String getFirstName();
    String getLastName();

    // Вычисляемое поле — склеивается на уровне Java, не SQL
    @Value("#{target.firstName + ' ' + target.lastName}")
    String getFullName(); // Hibernate загружает ВСЕ поля
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Почему опытные разработчики это ценят:&lt;/strong&gt;&amp;nbsp;Проекции — это одновременно паттерн безопасности и инструмент оптимизации. Одно объявление интерфейса устраняет и DTO-класс, и лишний трафик к базе данных.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Паттерн 4: Конвертные ответы для единообразных контрактов API&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Клиент вашего API делает десять запросов к десяти разным эндпоинтам. У каждого своя структура: один возвращает объект напрямую, другой оборачивает в `{ "data": ... }`, третий при ошибке отдаёт строку, четвёртый — объект с полем `error`. Frontend-разработчики пишут десять разных обработчиков. Новые члены команды не знают, что ожидать. Интеграция с партнёрами превращается в квест по документации.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Единый конверт ответа (Envelope Pattern) — универсальная обёртка, которая делает каждый эндпоинт предсказуемым.&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
//  Универсальный конверт ответа
public record ApiResponse&amp;lt;T&amp;gt;(
    boolean success,
    String message,
    T data,
    List&amp;lt;String&amp;gt; errors,
    Map&amp;lt;String, Object&amp;gt; meta
) {
    // Успешный ответ с данными
    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; ok(T data) {
        return new ApiResponse&amp;lt;&amp;gt;(true, "OK", data, null, null);
    }

    // Успешный ответ с данными и пагинацией
    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; ok(T data, Map&amp;lt;String, Object&amp;gt; meta) {
        return new ApiResponse&amp;lt;&amp;gt;(true, "OK", data, null, meta);
    }

    // Ответ об ошибке
    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; error(String message) {
        return new ApiResponse&amp;lt;&amp;gt;(false, message, null, null, null);
    }

    // Ответ с несколькими ошибками (например, ошибки валидации)
    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; validationError(List&amp;lt;String&amp;gt; errors) {
        return new ApiResponse&amp;lt;&amp;gt;(false, "Validation failed", null, errors, null);
    }
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Полный пример с пагинацией и обработкой ошибок&lt;/strong&gt;&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
// Контроллер
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;
    private final UserMapper userMapper;

    // Обычный запрос
    @GetMapping("/{id}")
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;UserResponse&amp;gt;&amp;gt; getUser(@PathVariable Long id) {
        UserResponse user = userMapper.toResponse(userService.findById(id));
        return ResponseEntity.ok(ApiResponse.ok(user));
    }

    // Запрос с пагинацией — мета-информация передаётся в конверте
    @GetMapping
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;List&amp;lt;UserResponse&amp;gt;&amp;gt;&amp;gt; getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {

        Page&amp;lt;User&amp;gt; usersPage = userService.findAll(PageRequest.of(page, size));

        Map&amp;lt;String, Object&amp;gt; meta = Map.of(
            "page", usersPage.getNumber(),
            "size", usersPage.getSize(),
            "totalElements", usersPage.getTotalElements(),
            "totalPages", usersPage.getTotalPages(),
            "last", usersPage.isLast()
        );

        return ResponseEntity.ok(
            ApiResponse.ok(userMapper.toResponseList(usersPage.getContent()), meta)
        );
    }
}&lt;/pre&gt;

&lt;pre class="brush:java;"&gt;
// Глобальный обработчик ошибок — ключевая часть паттерна
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse&amp;lt;Void&amp;gt; handleNotFound(EntityNotFoundException ex) {
        return ApiResponse.error(ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse&amp;lt;Void&amp;gt; handleValidation(MethodArgumentNotValidException ex) {
        List&amp;lt;String&amp;gt; errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(fe -&amp;gt; fe.getField() + ": " + fe.getDefaultMessage())
            .collect(Collectors.toList());
        return ApiResponse.validationError(errors);
    }

    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ApiResponse&amp;lt;Void&amp;gt; handleAccessDenied(AccessDeniedException ex) {
        return ApiResponse.error("Access denied");
    }
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Теперь клиент всегда получает предсказуемую структуру:&lt;/p&gt;

&lt;pre class="brush:jscript;"&gt;
// Успех
{
  "success": true,
  "message": "OK",
  "data": { "id": 1, "firstName": "Иван", "email": "ivan@example.com" },
  "errors": null,
  "meta": null
}

// Ошибка валидации
{
  "success": false,
  "message": "Validation failed",
  "data": null,
  "errors": ["email: must be a valid email", "firstName: must not be blank"],
  "meta": null
}

// Пагинация
{
  "success": true,
  "message": "OK",
  "data": [...],
  "errors": null,
  "meta": { "page": 0, "size": 20, "totalElements": 150, "totalPages": 8 }
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Важные детали&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Некоторые команды выбирают более лёгкий вариант без `success`/`errors` и просто полагаются на HTTP-статусы. Это тоже валидный подход. Главное — &lt;strong&gt;единообразие&lt;/strong&gt;: выбранный формат должен применяться ко всем эндпоинтам без исключений.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Почему опытные разработчики это ценят:&lt;/strong&gt;&amp;nbsp;Единообразие API — признак зрелой кодовой базы. Это разница между API, с которым внешние команды работают с удовольствием, и тем, интеграции с которым боятся.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Паттерн 5: @JsonView для ролевого формирования ответов&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Бизнес-требование: публичные пользователи должны видеть имя и email; менеджеры - также дату рождения и телефон; администраторы должны видеть&amp;nbsp;всё, включая внутренние заметки и историю входов. Самое простое&amp;nbsp;решение — создать три отдельных DTO: `UserPublicResponse`, `UserManagerResponse`, `UserAdminResponse`. Однако, при десяти ролях и двадцати сущностях это превращается в сотни DTO-классов, которые нужно синхронизировать при каждом изменении.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Jackson `@JsonView` позволяет управлять видимостью полей на уровне сериализации: одна сущность, несколько представлений, ноль дублирующихся классов.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Полный пример&lt;/strong&gt;&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
// Определение иерархии представлений
public class UserViews {
    // Иерархия через наследование:
    // Admin видит всё, что видит Manager,
    // Manager видит всё, что видит Public
    public interface Public {}
    public interface Manager extends Public {}
    public interface Admin extends Manager {}
}&lt;/pre&gt;

&lt;pre class="brush:java;"&gt;
// Сущность с разметкой полей
@Entity
@Table(name = "users")
public class User {

    @Id
    @JsonView(UserViews.Public.class)
    private Long id;

    @JsonView(UserViews.Public.class)
    private String firstName;

    @JsonView(UserViews.Public.class)
    private String lastName;

    @JsonView(UserViews.Public.class)
    private String email;

    // Видно менеджерам и выше
    @JsonView(UserViews.Manager.class)
    private String phone;

    @JsonView(UserViews.Manager.class)
    private LocalDate birthDate;

    @JsonView(UserViews.Manager.class)
    private String department;

    // Только для администраторов
    @JsonView(UserViews.Admin.class)
    private String internalNotes;

    @JsonView(UserViews.Admin.class)
    private LocalDateTime lastLoginAt;

    @JsonView(UserViews.Admin.class)
    private String lastLoginIp;

    @JsonView(UserViews.Admin.class)
    private boolean accountLocked;

    // Поля без @JsonView не попадают ни в один ответ
    private String passwordHash;
    private String resetToken;
}&lt;/pre&gt;

&lt;pre class="brush:java;"&gt;
// Контроллер с тремя представлениями
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    // Публичный профиль — для всех
    @GetMapping("/{id}/profile")
    @JsonView(UserViews.Public.class)
    public User getPublicProfile(@PathVariable Long id) {
        return userService.findById(id);
        // Ответ: id, firstName, lastName, email
    }

    // Расширенный профиль — для менеджеров
    @GetMapping("/{id}/details")
    @JsonView(UserViews.Manager.class)
    @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
    public User getUserDetails(@PathVariable Long id) {
        return userService.findById(id);
        // Ответ: id, firstName, lastName, email, phone, birthDate, department
    }

    // Полный профиль — только для администраторов
    @GetMapping("/admin/{id}")
    @JsonView(UserViews.Admin.class)
    @PreAuthorize("hasRole('ADMIN')")
    public User getAdminProfile(@PathVariable Long id) {
        return userService.findById(id);
        // Ответ: все поля, размеченные @JsonView(Admin.class) и выше
    }
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Важные детали&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Тестирование ролевой видимости&lt;/strong&gt;&amp;nbsp;— важная часть работы с `@JsonView`:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
@Test
void publicProfileShouldNotExposePhone() throws Exception {
    mockMvc.perform(get("/api/v1/users/1/profile"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.phone").doesNotExist())
        .andExpect(jsonPath("$.internalNotes").doesNotExist())
        .andExpect(jsonPath("$.email").exists());
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Программное переключение представления&lt;/strong&gt;&amp;nbsp;— когда роль определяется динамически:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
@GetMapping("/{id}")
public ResponseEntity&amp;lt;String&amp;gt; getUser(@PathVariable Long id) throws JsonProcessingException {
    User user = userService.findById(id);

    Class&amp;lt;?&amp;gt; view = SecurityUtils.isAdmin()
        ? UserViews.Admin.class
        : UserViews.Public.class;

    ObjectWriter writer = objectMapper.writerWithView(view);
    return ResponseEntity.ok(writer.writeValueAsString(user));
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ограничение паттерна:&amp;nbsp;&lt;/strong&gt;`@JsonView` применяется только к сериализации. Если вы возвращаете саму сущность, вы всё равно загружаете из базы все поля. Для оптимизации запросов к БД этот паттерн нужно комбинировать с проекциями (паттерн 3).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Почему опытные разработчики это ценят:&lt;/strong&gt;&amp;nbsp;Ролевое управление видимостью полей — требование безопасности в большинстве production-систем. `@JsonView` реализует его на уровне сериализации — самом надёжном месте, где невозможно случайно «забыть» применить фильтрацию.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Сравнение паттернов: когда что применять&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;| Паттерн | Лучше всего для | Компромисс |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;Record-классы&amp;nbsp;&lt;/strong&gt;| Любые DTO, быстрый старт | Ручное маппирование при простых случаях |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;MapStruct&lt;/strong&gt;&amp;nbsp;| Большие проекты, много сущностей | Требует настройки зависимости |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;Проекции&lt;/strong&gt;&amp;nbsp;| Эндпоинты списков и чтения | Ограниченная гибкость при сложных вычислениях |&lt;/p&gt;

&lt;p&gt;|&lt;strong&gt; Envelope&lt;/strong&gt;&amp;nbsp;| Публичные API, интеграции с партнёрами | Небольшой оверхед структуры ответа |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;@JsonView&lt;/strong&gt;&amp;nbsp;| Ролевой доступ к полям | Не оптимизирует запрос к БД |&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;На практике паттерны комбинируются. Зрелый проект обычно использует их все одновременно:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
// Все паттерны вместе
@GetMapping("/{id}")
@JsonView(UserViews.Public.class)           // Паттерн 5: ролевая фильтрация
public ApiResponse&amp;lt;UserResponse&amp;gt; getUser(@PathVariable Long id) {
    return ApiResponse.ok(                  // Паттерн 4: конверт ответа
        userMapper.toResponse(              // Паттерн 2: MapStruct
            userService.findById(id)
        )
    );
    // UserResponse — это Record (Паттерн 1)
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Заключение&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Возврат сырых сущностей из REST API — это не просто технический долг. Это ошибка проектирования с реальными последствиями: утечки данных в production, сломанные клиенты после рефакторинга схемы, бесконечные вопросы от frontend-команды «а что именно возвращает этот эндпоинт?».&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Каждый из пяти паттернов решает конкретную задачу:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Record-классы&amp;nbsp;— явный, самодокументируемый контракт ответа без шаблонного кода.&lt;/li&gt;
	&lt;li&gt;&amp;nbsp;MapStruct&amp;nbsp;— безопасное маппирование на этапе компиляции вместо молчаливых ошибок в runtime.&lt;/li&gt;
	&lt;li&gt;Проекции&amp;nbsp;— производительность и безопасность на уровне SQL-запроса.&lt;/li&gt;
	&lt;li&gt;Envelope-обёртки&amp;nbsp;— единообразие, которое внешние команды перестают замечать, потому что «оно просто работает».&lt;/li&gt;
	&lt;li&gt;@JsonView&amp;nbsp;— ролевое управление видимостью без взрыва DTO-классов.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Начать можно с малого. Если сегодня ваши контроллеры возвращают сырые сущности — введите Record-классы. Это займёт немного времени&amp;nbsp;и сразу закроет риск утечки полей. Затем добавьте MapStruct. Envelope-паттерн введите перед первой внешней интеграцией. Проекции — когда появятся жалобы на производительность списков. `@JsonView` — как только появятся разные роли пользователей.&lt;/p&gt;</summary>
    <dc:creator>Romo Fedoroff</dc:creator>
    <dc:date>2026-04-01T09:28:00Z</dc:date>
  </entry>
  <entry>
    <title>Поломалась оснастка порты управляемых сетей в zVirt</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21235647" />
    <author>
      <name>Andrei Maksimov</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21235647</id>
    <updated>2026-03-30T11:39:55Z</updated>
    <published>2026-03-30T10:48:00Z</published>
    <summary type="html">&lt;p&gt;В какой то момент в zVirt поломалась возможность импортировать виртуальные машины из образов в формате OVA размещённых на дисках хоста с гипервизором. Но о том как это чинить, как-нибудь в другой раз, а может и в очередном обновлении починится.&lt;br /&gt;
Дело в том, неудачные попытки импорта имеют неприятные последствия. В интерфейсе настройки Управляемых сетей на вкладке Порты нас встречает сообщение вида.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;«Порт с идентификатором c7808f18-b5ab-4045-a230-c4ce59a86139 не найден»&lt;br /&gt;
​​​​​​​&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;И это все, что&amp;nbsp; мы можем наблюдать на этой вкладке, соответственно&amp;nbsp; управлять портами из веб-интерфейса становиться не возможно.&amp;nbsp;&lt;br /&gt;
Как показало небольшое исследование, данная проблема возникает&amp;nbsp; вследствие неудачного импорта. Во время импорта создаётся новая ВМ , для неё создаётся порт в&amp;nbsp; соответствующей логической сети SDN. Но после краха процедуры импорта, виртуальная машина из конфигурации zvirt удаляется, а вот в базе данных программно определяемых сетей zvirt остаётся, что и вызывает вышеуказанное сообщение.&lt;br /&gt;
Чтобы исправить ситуацию сначала нужно найти какому порту соответствует указанный в ошибке идентификатор. Все парамеры портов хранятся в базе данных OVN , которую можно посмотреть на менеджере виртуализации.&lt;br /&gt;
Искомый идентификатор задаётся в параметре&amp;nbsp; &amp;nbsp;ovirt_device_id в поле external_ids в свойствах порта. Для поиска нужного порта можно использовать команду&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ovn-nbctl find logical_switch_port external_ids:ovirt_device_id=&amp;lt;ИД из ошибки&amp;gt;&lt;/em&gt;&lt;/p&gt;

&lt;pre class="brush:bash;"&gt;
ovn-nbctl find logical_switch_port external_ids:ovirt_device_id=&lt;strong&gt;c7808f18-b5ab-4045-a230-c4ce59a86139&lt;/strong&gt; 
_uuid               : e7ff4956-2151-4190-aee4-4d5681691050
 addresses           : ["56:6f:7e:2b:00:a2"]
 dhcpv4_options      : []
 dhcpv6_options      : []
 dynamic_addresses   : []
 enabled             : true external_ids        : {ovirt_device_id="c7808f18-b5ab-4045-a230-c4ce59a86139", ovirt_device_owner=oVirt, ovirt_nic_name=nic1, ovirt_security_groups="", zvirt_mode=dynamic, zvirt_namespace=common}
 ha_chassis_group    : []
 mirror_rules        : []
 &lt;strong&gt;name&lt;/strong&gt;                : "&lt;strong&gt;c9afb0db-de90-4a42-a26c-88e69eb4c183&lt;/strong&gt;"
 options             : {}
 parent_name         : []
 port_security       : []
 tag                 : [] 
tag_request         : [] 
type                : ""
 up                  : false&lt;/pre&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;В поле&amp;nbsp; name&amp;nbsp; указано имя порта.&lt;br /&gt;
На всякий случай можно проверить в базе данных менеджера нет ли ВМ использующей этот порт, по имени порта или MAC адресу.&amp;nbsp; Я просто поискал в дампе базы данных извлечённой из резервной копии (о том как делать резервные копии мы подробно рассказываем &lt;a id="backup" name="backup"&gt;&lt;/a&gt;&lt;a href="https://www.tune-it.ru/education/catalogue/-/catalogue/vendors/%D0%9E%D1%80%D0%B8%D0%BE%D0%BD/zVirt/zvirt-base" target="_blank"&gt;тут&lt;/a&gt;) и увидел такую запись&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&lt;/p&gt;

&lt;pre class="brush:bash;"&gt;
grep  05ef7f198d82   out_engine 21703    00000000-0000-0000-0000-000000000000    SYSTEM    \N        \N        \N        2026-03-25 11:24:02.987+03    SDN_PORT_CREATE_SUCCESS    16301    0    Port nic1 (9223a5aa-8394-4caf-9a33-05ef7f198d82) for VM DC602-location1 created successfully    f    \N        \N        \N        \N    \N    \N        \N        oVirt    \N    \N    \N    f    \N    \N    \N    \N&lt;/pre&gt;

&lt;p&gt;&lt;br /&gt;
Что усилило мои подозрения.&lt;br /&gt;
Теперь фантомный порт необходимо удалить из базы данных OVN. Перед удаление настоятельно рекомендуется выполнить&amp;nbsp; &lt;a href="http://www.tune-it.ru/education/catalogue/-/catalogue/vendors/%D0%9E%D1%80%D0%B8%D0%BE%D0%BD/zVirt/zvirt-base" target="_blank"&gt;резервное копирование&lt;/a&gt; конфигурации менеджера.&lt;br /&gt;
Удаление выполняется с помощью команды &lt;em&gt;ovn-nbctl lsp-del &amp;lt;имя порта &amp;gt;&lt;/em&gt;&lt;br /&gt;
​​​​​​​&lt;/p&gt;

&lt;pre class="brush:bash;"&gt;
ovn-nbctl lsp-del c9afb0db-de90-4a42-a26c-88e69eb4c183&lt;/pre&gt;

&lt;p&gt;Возможно&amp;nbsp; создалось несколько фантомных портов , эту процедуру нужно повторить для каждого.&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Andrei Maksimov</dc:creator>
    <dc:date>2026-03-30T10:48:00Z</dc:date>
  </entry>
  <entry>
    <title>Невидимые пользователи: Проектируем цифровую среду, доступную каждому</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21227823" />
    <author>
      <name>Алексей Кондратьев</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21227823</id>
    <updated>2026-03-09T16:28:05Z</updated>
    <published>2026-03-09T14:51:00Z</published>
    <summary type="html">&lt;p&gt;Вы когда-нибудь пробовали пользоваться любимым сайтом с закрытыми глазами? Или только одной рукой? А может быть с громко играющей музыкой в наушниках, которая мешает сосредоточиться на интерфейсе? В такие моменты каждый из нас хотя бы отчасти приближается к пониманию того, что испытывают люди с инвалидностью ежедневно.&lt;br /&gt;
&lt;br /&gt;
Доступность (Accessibility, часто сокращаемая как A11y — где 11 означает количество букв между 'A' и 'y') — это не просто моральный долг или соответствие юридическим нормам. Это фундаментальное качество продукта, определяющее, сможет ли им воспользоваться каждый пятый житель планеты.&lt;br /&gt;
&lt;br /&gt;
В данной статье мы рассмотрим международный стандарт WCAG 2.2, научимся проектировать для разных групп пользователей и разберем практические инструменты тестирования, включая работу со скрин-ридерами.&lt;/p&gt;

&lt;h3 dir="ltr"&gt;&lt;b id="docs-internal-guid-34be1f07-7fff-d9e6-ed8c-a5fcde1c8547"&gt;Часть 1: WCAG 2.2 — Навигационная карта доступности&lt;/b&gt;&lt;/h3&gt;

&lt;p dir="ltr"&gt;Web Content Accessibility Guidelines (WCAG) — это «золотой стандарт» доступности, разработанный Консорциумом Всемирной паутины (W3C). В октябре 2023 года была утверждена новая редакция — WCAG 2.2, которая пришла на смену версии 2.1.&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;strong&gt;1.1. Четыре принципа POUR&lt;/strong&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;В основе WCAG лежат четыре фундаментальных принципа, известных по аббревиатуре POUR. Контент должен быть:&lt;/p&gt;

&lt;ol dir="ltr"&gt;
	&lt;li&gt;Воспринимаемым (Perceivable): Пользователи должны иметь возможность воспринимать информацию, даже если у них есть сенсорные ограничения. Информация не может быть невидимой для всех органов чувств сразу .&lt;/li&gt;
	&lt;li&gt;Управляемым (Operable): Интерфейс должен работать с разными устройствами ввода. Пользователь должен иметь возможность управлять интерфейсом (например, нажимать кнопки), даже если не может использовать мышь .&lt;/li&gt;
	&lt;li&gt;Понятным (Understandable): И информация, и управление интерфейсом должны быть ясными. Пользователь должен понимать, где он находится и что происходит на странице.&lt;/li&gt;
	&lt;li&gt;Надёжным (Robust): Контент должен быть совместим с различными пользовательскими агентами, включая ассистивные технологии (скрин-ридеры, брайлевские дисплеи) .&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-3a068bcf-7fff-023c-d976-56f4caa94f04"&gt;1.2. Что нового в WCAG 2.2?&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Главное изменение в версии 2.2 — фокус на пользователей с ограниченной моторикой и когнитивными нарушениями . Добавлено 9 новых критериев, а один устаревший исключен . Вот ключевые нововведения, на которые стоит обратить внимание:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Фокус не должен исчезать (Focus Appearance — уровень AA): Визуальный индикатор фокуса клавиатуры (обычно контур вокруг элемента) должен иметь достаточный контраст и размер, чтобы его было легко заметить.&lt;/li&gt;
	&lt;li&gt;Перетаскивание (Dragging — уровень AA): Действия, требующие перетаскивания объектов (drag &amp;amp; drop), должны иметь альтернативный простой способ выполнения (например, нажатие кнопок), так как многие пользователи с моторными нарушениями не могут удерживать кнопку мыши при перемещении.&lt;/li&gt;
	&lt;li&gt;Целевой размер (Target Size — уровень AA): Для интерактивных элементов (кнопок, ссылок) рекомендуется минимальный размер 24x24 пикселя, чтобы по ним было легко попасть людям с тремором рук или при использовании мобильных устройств. Исключения делается для ссылок внутри текстового абзаца.&lt;/li&gt;
	&lt;li&gt;Аутентификация (Accessible Authentication — уровень AA): Процессы входа в систему не должны требовать решения задач, основанных на запоминании пароля или распознавании объектов (капча), если для этого нет альтернативы. Это критически важно для людей с когнитивными нарушениями.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Понимание WCAG 2.2 становится обязательным не только для госсектора. С 28 июня 2025 года Европейский акт о доступности (EAA) распространяет требования уровня AA на частный бизнес в ЕС: интернет-магазины, банки, телеком-операторов и многих других .&lt;/p&gt;

&lt;h3 dir="ltr"&gt;&lt;b id="docs-internal-guid-c33297bd-7fff-0449-86ed-e663fe5dbb7c"&gt;Часть 2: Дизайн для всех — учет особенностей пользователей&lt;/b&gt;&lt;/h3&gt;

&lt;p dir="ltr"&gt;Универсальный дизайн начинается с эмпатии. Рассмотрим, как потребности разных групп пользователей влияют на наши решения.&lt;/p&gt;

&lt;h4 dir="ltr"&gt;&lt;b id="docs-internal-guid-79ce8847-7fff-5059-b2dd-9c449290f8dc"&gt;2.1. Нарушения зрения (от слепоты до дальтонизма)&lt;/b&gt;&lt;/h4&gt;

&lt;p dir="ltr"&gt;Это самая очевидная аудитория для применения стандартов доступности. По данным ВОЗ, более 2,2 млрд человек имеют те или иные нарушения зрения.&lt;/p&gt;

&lt;p dir="ltr"&gt;Практические советы:&lt;/p&gt;

&lt;ul dir="ltr"&gt;
	&lt;li&gt;Контрастность: Обеспечьте достаточный контраст между текстом и фоном. Для обычного текста на уровне AA требуется контраст 4.5:1, для крупного текста — 3:1 . Используйте инструменты проверки контраста (Color Contrast Analyser, WebAIM).&lt;/li&gt;
	&lt;li&gt;Визуальные сигналы: Не используйте цвет как единственный сигнал, дающий информацию о состоянии объекта. Статус системы должен быть визуально понятным даже в черно-белом исполнении. Простыми словами, дублируйте сигнал иконками, текстом или подчеркиванием .&lt;/li&gt;
	&lt;li&gt;Текстовые альтернативы: Все значимые изображения должны иметь атрибут alt с описанием содержания. Декоративные изображения должны быть скрыты от скрин-ридеров (пустой alt="") , .&lt;/li&gt;
	&lt;li&gt;Масштабируемость: Интерфейс должен корректно отображаться при увеличении экрана до 400% без потери функциональности.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-5bbc3894-7fff-2c36-bd3b-0e4fc709102c"&gt;2.2. Нарушения моторики&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Сюда входят люди с параличом, тремором рук, артритом, а также те, у кого временно сломана рука. Они часто используют клавиатуру, трекбол или специальные устройства (стикеры для рта).&lt;/p&gt;

&lt;p&gt;Что делать дизайнеру:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Управление с клавиатуры: Все функции, доступные мышью, должны быть доступны с клавиатуры. Это означает логичный порядок фокуса (обычно совпадающий с визуальным порядком на странице) и видимый индикатор фокуса .&lt;/li&gt;
	&lt;li&gt;Крупные кликабельные области: Как требует WCAG 2.2, кнопки и ссылки должны быть большими, чтобы в них было легко попасть .&lt;/li&gt;
	&lt;li&gt;Достаточно времени: Избегайте таймеров или давайте возможность их продлить/отключить. Людям с моторными нарушениями может потребоваться больше времени на заполнение формы.&lt;/li&gt;
	&lt;li&gt;Отказ от сложных жестов: Предоставляйте простую альтернативу сложным жестам (смахивание, мультитач).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-ce9e407f-7fff-ce7a-db8f-f0d010d4c8ab"&gt;2.3. Когнитивные особенности&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Это одна из самых сложных и разнообразных групп. Она включает людей с нарушениями обучаемости (дислексия), памяти, внимания, а также пожилых людей.&lt;/p&gt;

&lt;p&gt;Что делать на практике:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Простота и предсказуемость: Интерфейс должен быть последовательным. Навигация и кнопки должны находиться на привычных местах. Избегайте неожиданных переходов и всплывающих окон.&lt;/li&gt;
	&lt;li&gt;Понятный язык: Используйте короткие предложения, избегайте сложных терминов и жаргона. Расшифровывайте аббревиатуры . Пишите "Январь" вместо "Янв".&lt;/li&gt;
	&lt;li&gt;Четкая структура: Используйте заголовки, списки и иконки для визуального разделения информации . Пользователю должно быть легко сканировать страницу взглядом.&lt;/li&gt;
	&lt;li&gt;Упрощенная аутентификация: Дайте возможность войти по биометрии или с помощью ссылки на email вместо запоминания сложного пароля.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 dir="ltr"&gt;&lt;b id="docs-internal-guid-26556fa8-7fff-d258-19a4-58b673590ef3"&gt;Часть 3: Техническая реализация и семантическая верстка&lt;/b&gt;&lt;/h3&gt;

&lt;p dir="ltr"&gt;Красивый дизайн бесполезен, если он не может быть корректно передан ассистивным технологиям. Здесь на сцену выходит семантический HTML.&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;b id="docs-internal-guid-9bbba552-7fff-89e6-70bf-20c261e47162"&gt;3.1. Почему семантика — это основа?&lt;/b&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;Семантическая верстка — это использование HTML-элементов строго по их назначению. Это язык, на котором сайт общается с браузером, поисковиками и, что важнее всего, со скрин-ридерами.&lt;/p&gt;

&lt;p dir="ltr"&gt;Например, можно сделать "кнопку" так:&lt;/p&gt;

&lt;pre class="brush:jscript;"&gt;
&lt;code&gt;&amp;lt;div class="btn" role="button" tabindex="0"&amp;gt;Купить&amp;lt;/div&amp;gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;Но гораздо правильнее и проще так:&lt;/p&gt;

&lt;pre class="brush:jscript;"&gt;
&lt;code&gt;&amp;lt;button&amp;gt;Купить&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Элемент &amp;lt;button&amp;gt;&amp;nbsp;«из коробки» имеет правильную роль, управление с клавиатуры (Tab, Enter/Пробел) и фокус .&lt;/p&gt;

&lt;p&gt;Ключевые семантические теги:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;&amp;lt;header&amp;gt;, &amp;lt;nav&amp;gt;, &amp;lt;main&amp;gt;, &amp;lt;footer&amp;gt;, &amp;lt;aside&amp;gt; — создают навигационные вехи (landmarks), по которым пользователи скрин-ридеров могут быстро перемещаться.&lt;/li&gt;
	&lt;li&gt;&amp;lt;h1&amp;gt; — &amp;lt;h6&amp;gt; — создают иерархию заголовков. Никогда не пропускайте уровни (не перескакивайте с &amp;lt;h2&amp;gt; на &amp;lt;h4&amp;gt;). &amp;lt;h1&amp;gt; должен быть на странице один.&lt;/li&gt;
	&lt;li&gt;&amp;lt;ul&amp;gt;, &amp;lt;ol&amp;gt;, &amp;lt;li&amp;gt; — для списков.&lt;/li&gt;
	&lt;li&gt;&amp;lt;table&amp;gt; с &amp;lt;th&amp;gt; — для таблиц с данными (указывайте заголовки столбцов/строк).&lt;/li&gt;
	&lt;li&gt;&amp;lt;a&amp;gt; — для ссылок. Текст ссылки должен быть понятен вне контекста (никогда не пишите "нажмите здесь") .&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-45ff3bb0-7fff-831c-dd07-51db34868674"&gt;3.2. Доступные формы&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Формы — частый источник проблем. Чтобы сделать их доступными:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Каждый &amp;lt;input&amp;gt;, &amp;lt;select&amp;gt; или &amp;lt;textarea&amp;gt; должен быть связан с подписью через связку id и &amp;lt;label for="id"&amp;gt; или быть вложенным в &amp;lt;label&amp;gt;.&lt;/li&gt;
	&lt;li&gt;Группируйте логически связанные поля (например, адрес) в &amp;lt;fieldset&amp;gt; и давайте группе имя через &amp;lt;legend&amp;gt;.&lt;/li&gt;
	&lt;li&gt;Четко обозначайте обязательные поля и формат ввода данных в подписях, а не только цветом.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-764a96ab-7fff-fecf-5816-e0cbd63bae44"&gt;3.3. Когда HTML не справляется: WAI-ARIA&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Иногда мы создаем сложные интерфейсные компоненты (например, кастомный выпадающий список с автодополнением), для которых нет подходящего HTML-тега. В таких случаях на помощь приходит WAI-ARIA (Accessible Rich Internet Applications).&lt;/p&gt;

&lt;p&gt;ARIA позволяет добавить к элементам атрибуты, которые сообщают скрин-ридеру дополнительную информацию:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Роль (role): Что это за элемент? (role="dialog", role="tablist", role="progressbar").&lt;/li&gt;
	&lt;li&gt;Состояние и свойства: В каком он состоянии? (aria-expanded="true/false", aria-checked="true", aria-hidden="true").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Важное правило: Не использовать ARIA там, где можно обойтись нативным HTML. ARIA — это как хирургический инструмент для исправления сложных случаев, а не замена простым и понятным тегам.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/ru/docs/Learn_web_development/Core/Accessibility/HTML"&gt;Подробнее о создании доступной среды благодаря HTML&lt;/a&gt;&lt;/p&gt;

&lt;h3 dir="ltr"&gt;&lt;b id="docs-internal-guid-e45b7a04-7fff-229b-e490-840703dd8d06"&gt;Часть 4: Тестирование — Смотрим и слушаем мир чужими глазами&lt;/b&gt;&lt;/h3&gt;

&lt;p dir="ltr"&gt;Автоматические инструменты (Lighthouse, axe) — отличный первый шаг, но они находят лишь около 30% проблем. Настоящее тестирование доступности — это ручной труд и эмпатия.&lt;/p&gt;

&lt;h4 dir="ltr"&gt;&lt;b id="docs-internal-guid-7e1670ac-7fff-75ba-9d71-0f887a88f4ee"&gt;4.1. Тестирование клавиатурой&lt;/b&gt;&lt;/h4&gt;

&lt;p dir="ltr"&gt;Самый простой и эффективный тест. Отключите мышь и попробуйте пользоваться сайтом только с клавиатуры.&lt;/p&gt;

&lt;ol dir="ltr"&gt;
	&lt;li&gt;Используйте клавишу Tab, чтобы перемещаться по интерактивным элементам.&lt;/li&gt;
	&lt;li&gt;Всегда ли видно, где находится фокус (синяя или пунктирная обводка)?&lt;/li&gt;
	&lt;li&gt;Логичен ли порядок перехода? Не прыгает ли фокус с главного меню сразу в подвал?&lt;/li&gt;
	&lt;li&gt;Можно ли открыть все выпадающие списки, выбрать пункт и закрыть их?&lt;/li&gt;
	&lt;li&gt;Можно ли активировать все кнопки пробелом или энтером?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-856e49e8-7fff-e70c-1049-1bd9ab4fe5e1"&gt;4.2. Тестирование со скрин-ридерами&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/training/modules/develop-products-with-screen-reader-support/7-test-screen-reader-support"&gt;Скрин-ридеры&lt;/a&gt; (читалки экрана) преобразуют цифровой текст в синтезированную речь или шрифт Брайля. Это основной инструмент для незрячих пользователей. Тестирование с ними — обязательный этап.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Популярные комбинации скрин-ридеров:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Windows: NVDA (бесплатный, лучше всего работает с Firefox). Или JAWS (платный, самый популярный в корпоративном секторе) .&lt;/li&gt;
	&lt;li&gt;macOS: VoiceOver (встроен в систему, лучше всего работает с Safari).&lt;/li&gt;
	&lt;li&gt;Android: TalkBack (встроен).&lt;/li&gt;
	&lt;li&gt;iOS: VoiceOver (встроен).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Что проверять с помощью скрин-ридера:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Чтение содержимого: Пройдитесь по странице стрелками (режим виртуального курсора). Все ли элементы озвучиваются? Правильно ли озвучиваются изображения (через alt)?&lt;/li&gt;
	&lt;li&gt;Навигация по заголовкам (клавиша H): Можете ли вы составить "карту" страницы и перейти к нужному разделу только по заголовкам? Не пропущены ли уровни? .&lt;/li&gt;
	&lt;li&gt;Навигация по ссылкам (клавиша K): Понятно ли, куда ведут ссылки, при прослушивании их вне контекста?&lt;/li&gt;
	&lt;li&gt;Навигация по вехам (landmarks) (клавиша D): Можно ли быстро перейти к основному контенту (&amp;lt;main&amp;gt;), минуя шапку и навигацию?&lt;/li&gt;
	&lt;li&gt;Работа с формами (клавиши F, E, C, R, B): Озвучивается ли подпись поля, когда вы входите в него? Правильно ли читаются сообщения об ошибках?&lt;/li&gt;
	&lt;li&gt;Динамический контент: Оповещает ли скрин-ридер о появлении всплывающих окон или обновлении части страницы?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-f7d1a99a-7fff-fef1-08c7-eea1b4f4372a"&gt;4.3. Инструменты для помощи в тестировании&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Помимо ручного тестирования, полезно использовать:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Accessibility Insights for Web (браузерный плагин): Помогает проходить проверки пошагово.&lt;/li&gt;
	&lt;li&gt;Lighthouse (встроен в Chrome DevTools): Быстрая автоматическая проверка основных метрик.&lt;/li&gt;
	&lt;li&gt;Инструменты разработчика (браузера): Позволяют инспектировать дерево доступности (Accessibility Tree), чтобы увидеть, какую именно информацию браузер передает скрин-ридеру.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-369dd0be-7fff-cc0a-6316-1ae9dc35a9a5"&gt;Заключение&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Доступность — это не финальный штрих и не чек-лист для галочки. Это философия проектирования, которая ставит человека в центр вселенной продукта. Интегрируя знания о WCAG 2.2, создавая продуманный дизайн для разных групп пользователей, опираясь на семантическую верстку и проверяя свою работу с реальными инструментами (от клавиатуры до VoiceOver), мы перестаем делить мир на "обычных" и "особенных" пользователей. Мы просто создаем качественный, удобный и честный продукт для всех.&lt;/p&gt;</summary>
    <dc:creator>Алексей Кондратьев</dc:creator>
    <dc:date>2026-03-09T14:51:00Z</dc:date>
  </entry>
  <entry>
    <title>Как я сократил время итерации в 3 раза на проекте с микросервисами</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21224493" />
    <author>
      <name>Maxim Kalabukhov</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21224493</id>
    <updated>2026-03-04T11:28:24Z</updated>
    <published>2026-03-04T09:33:00Z</published>
    <summary type="html">&lt;p&gt;Рано или поздно любая задача по написанию кода сводится к одному и тому же циклу: собрал проект -&amp;gt; запустил -&amp;gt; прогнал тесты -&amp;gt; поймал проблему -&amp;gt; выдвинул гипотезу -&amp;gt; внёс правки -&amp;gt; и снова по кругу, каждый раз надеясь, что в этот раз «точно всё».&lt;/p&gt;

&lt;p&gt;На моём проекте - 10+ микросервисов на kotlin/spring, docker, gradle - один такой круг занимал примерно 3 минуты: секунд 15–30 на сборку всего проекта, около 2 минут на запуск контейнеров и ещё секунд 30 на тестирование. Казалось бы, 3 минуты - мелочь. Но представьте, что за одну задачу вы проходите этот цикл 10 раз (а это вполне реалистично). Получается 30 минут чистого ожидания - просто сидишь и смотришь в монитор. Мне показалось, что с этим стоит что-то сделать.&lt;/p&gt;

&lt;p&gt;Первая мысль была в духе «надо как-то ускорить сборку». Но потом я понял, что проблема не в скорости отдельных шагов, а в том, что на каждой итерации собиралось и запускалось всё. Все 10+ сервисов. Каждый раз. Хотя для проверки моей гипотезы обычно нужны один-два из них.&lt;/p&gt;

&lt;p&gt;Вопрос переформулировался: как сделать так, чтобы на каждую итерацию собиралось и поднималось только то, что действительно нужно?&lt;/p&gt;

&lt;p&gt;До этого я запускал gradle из терминала, это была прям укоренившаяся привычка. Но оказалось, что idea собирает gradle-проект заметно быстрее. Уже одно это дало заметный выигрыш. Но главное в idea сборку можно встроить в единый пайплайн с запуском контейнеров.&lt;/p&gt;

&lt;h3&gt;Всё в одной конфигурации&lt;/h3&gt;

&lt;p&gt;В idea можно создать docker сompose конфигурацию, которая за один запуск делает всё: собирает нужные модули и поднимает контейнеры. Вот как это выглядит:&lt;/p&gt;

&lt;p&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21214588/Pasted+image+20260304123936.png/5df3246d-9290-ba63-d649-395662845c39?imagePreview=1" /&gt;&lt;/p&gt;

&lt;p&gt;Тут указываем путь до docker compose файла, а в &lt;code&gt;Services&lt;/code&gt; вместо указания 10+ сервисов, перечисляем только те, которые нужны для текущей задачи, по желанию указываем флаги для &lt;code&gt;docker compose up&lt;/code&gt;. В &lt;code&gt;Before-launch&lt;/code&gt; добавляем gradle-таски &lt;code&gt;clean assemble&lt;/code&gt;, но только для конкретных модулей - это позволит idea выполнить их перед тем как запустить &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Экономим энергию и пару секунд хоткеем &lt;code&gt;Shift+F10&lt;/code&gt; и радуемся результату - idea сначала собрала два нужных модуля, потом подняла два контейнера. Всё. Не нужно помнить порядок команд, не нужно их прописывать вручную.&lt;/p&gt;

&lt;p&gt;Вместо одной «универсальной» конфигурации, которая поднимает всё, я создаю отдельную под каждую задачу. Работаю над gateway и some - конфигурация &lt;code&gt;gateway+some&lt;/code&gt;. Завтра нужен другой набор сервисов - создаю новую.&amp;nbsp;Создание такой конфигурации занимает пару минут. Но эта пара минут окупается уже на второй итерации.&lt;/p&gt;

&lt;p&gt;Давайте глянем на цифры: сборка теперь занимает в среднем 5 секунд, а запуск 15 секунд. Итого мне нужно 50 секунд на проверку гипотезы, вместо 180, а это разница в 3 раза и это ощущается - стало легче сохранять фокус и контекст во время ожидания.&lt;/p&gt;

&lt;p&gt;Если у вас похожий стек - kotlin/spring, gradle, docker, несколько микросервисов - попробуйте. Возможно, самое дорогое время в вашем рабочем дне - это не написание кода, а ожидание, пока он соберётся.&lt;/p&gt;</summary>
    <dc:creator>Maxim Kalabukhov</dc:creator>
    <dc:date>2026-03-04T09:33:00Z</dc:date>
  </entry>
  <entry>
    <title>HashMap в Java: 10 фактов, о которых вы, возможно, не знали</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21224460" />
    <author>
      <name>Polina Napolskaya</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21224460</id>
    <updated>2026-03-04T09:04:06Z</updated>
    <published>2026-03-04T08:14:00Z</published>
    <summary type="html">&lt;p&gt;HashMap – наверное, самая используемая коллекция в Java. Мы кладём в неё элементы, достаём, редко задумываясь, что происходит под капотом. И в целом это нормально: класс работает, документация есть, сообщество стабильно советует переопределять &lt;code&gt;equals&lt;/code&gt; и &lt;code&gt;hashCode&lt;/code&gt;. Но если копнуть чуть глубже, обнаруживается немало деталей, которые могут удивить даже опытного разработчика.&lt;/p&gt;

&lt;p&gt;Я собрала десять таких моментов. Не про то, как устроен HashMap в целом, а про то, что обычно остаётся за скобками.&lt;/p&gt;

&lt;h2&gt;1. Почему порог деревизации – именно 8&lt;/h2&gt;

&lt;p&gt;С Java 8, когда в одной корзине становится больше восьми элементов, связанный список преобразуется в красно-чёрное дерево. Логика понятна: начиная с какого-то размера список работает слишком медленно. Но почему порог установлен именно на восьми?&lt;/p&gt;

&lt;p&gt;Ответ лежит в теории вероятностей. Разработчики Oracle смоделировали ситуацию с хорошей хеш-функцией и стандартным нагрузочным фактором 0.75. Согласно распределению Пуассона, вероятность того, что в одну корзину попадёт восемь элементов, составляет примерно 0.00000006. Это шесть случаев на десять миллионов.&lt;/p&gt;

&lt;p&gt;Восемь здесь – не случайное число, а своеобразная «защита от дурака». Если коллизий так много, значит, либо хеш-функция работает плохо, либо кто-то пытается организовать атаку. В штатной же ситуации дерево просто не нужно. Кстати, обратный переход из дерева в список происходит, когда элементов становится меньше шести – это предотвращает частые преобразования при добавлении и удалении.&lt;/p&gt;

&lt;h2&gt;2. Сдвиг на 16 бит: зачем он нужен&lt;/h2&gt;

&lt;p&gt;В исходном коде HashMap есть такая строчка:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;(h = key.hashCode()) ^ (h &amp;gt;&amp;gt;&amp;gt; 16)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;На первый взгляд кажется лишней. Зачем перед вычислением индекса ещё что-то делать с хеш-кодом?&lt;/p&gt;

&lt;p&gt;Дело в том, что размер внутреннего массива всегда является степенью двойки. Индекс корзины вычисляется не через взятие остатка, а через побитовое И: &lt;code&gt;(n - 1) &amp;amp; hash&lt;/code&gt;. Это быстро, но есть ограничение: в вычислении участвуют только младшие биты хеш-кода. Старшие, по сути, игнорируются.&lt;/p&gt;

&lt;p&gt;Сдвиг на 16 бит и XOR решают эту проблему. Они «перемешивают» старшие и младшие биты, и даже если у двух объектов хеш-коды различаются только в старшей части, они всё равно попадут в разные корзины.&lt;/p&gt;

&lt;h2&gt;3. Массив создаётся не сразу&lt;/h2&gt;

&lt;p&gt;Когда вы пишете &lt;code&gt;new HashMap&amp;lt;&amp;gt;()&lt;/code&gt;, внутренний массив ещё не существует. Он создаётся только при первой вставке элемента. До этого объект занимает минимум памяти – фактически только накладные расходы на сам экземпляр.&lt;/p&gt;

&lt;p&gt;Это сделано намеренно: если мапа объявлена, но не используется, память не тратится зря. Мелочь, но в масштабах крупного приложения такие мелочи дают ощутимый выигрыш.&lt;/p&gt;

&lt;h2&gt;4. Начальная ёмкость считается не так, как вы думаете&lt;/h2&gt;

&lt;p&gt;Конструктор &lt;code&gt;new HashMap&amp;lt;&amp;gt;(100)&lt;/code&gt; часто понимают неправильно. Кажется, что если передать 100, то можно положить 100 элементов без расширения. На самом деле 100 – это размер внутреннего массива. Порог срабатывает при превышении &lt;code&gt;ёмкость * loadFactor&lt;/code&gt;, то есть при 75 элементах.&lt;/p&gt;

&lt;p&gt;Чтобы без расширения поместилось ровно 100 элементов, раньше нужно было писать &lt;code&gt;(int) Math.ceil(100 / 0.75)&lt;/code&gt;. Выглядит как заклинание… К счастью, в Java 19 появился метод &lt;code&gt;HashMap.newHashMap(100)&lt;/code&gt;, который делает этот расчёт автоматически.&lt;/p&gt;

&lt;h2&gt;5. Итерация зависит от ёмкости, а не только от размера&lt;/h2&gt;

&lt;p&gt;В документации сказано: время итерации пропорционально ёмкости плюс размер. Это не просто формальность.&lt;/p&gt;

&lt;p&gt;Если вы создали мапу с ёмкостью 10 000, но положили в неё два элемента, итератор всё равно обойдёт все 10 000 корзин, проверяя каждую на наличие данных. Пустые корзины не пропускаются. Поэтому если вы планируете часто перебирать элементы, не стоит завышать начальную ёмкость без необходимости.&lt;/p&gt;

&lt;h2&gt;6. putAll может работать медленнее ручного копирования&lt;/h2&gt;

&lt;p&gt;Неожиданный факт, но в некоторых версиях Java для больших мап метод&amp;nbsp;&lt;code&gt;putAll&lt;/code&gt; (и конструктор копирования) работает медленнее, чем обычный цикл по &lt;code&gt;entrySet() &lt;/code&gt;с ручным добавлением.&lt;/p&gt;

&lt;p&gt;Исследователи OpenJDK связывают это с тем, что JIT-компилятор не всегда может эффективно оптимизировать полиморфные вызовы на границах методов. В простом же цикле оптимизаций больше, и код выполняется быстрее – разница может достигать 20–30%. Ситуация варьируется от версии к версии, но о ней полезно знать, если вы работаете с действительно большими объёмами данных.&lt;/p&gt;

&lt;h2&gt;7. Ключ null всегда попадает в нулевую корзину&lt;/h2&gt;

&lt;p&gt;То, что HashMap допускает один ключ &lt;code&gt;null&lt;/code&gt;, знают все. Но мало кто задумывается, куда именно он попадает.&lt;/p&gt;

&lt;p&gt;В методе &lt;code&gt;hash()&lt;/code&gt; есть явная проверка: для &lt;code&gt;null &lt;/code&gt;возвращается 0. При вычислении индекса &lt;code&gt;(n - 1) &amp;amp; 0 &lt;/code&gt;результат всегда будет 0. То есть все ключи &lt;code&gt;null&lt;/code&gt; хранятся строго в нулевой корзине и всегда располагаются первыми в списке или дереве этой корзины.&lt;/p&gt;

&lt;h2&gt;8. Бесконечный цикл в Java 7 больше не актуален&lt;/h2&gt;

&lt;p&gt;В Java 7 и более ранних версиях в многопоточной среде при одновременном расширении HashMap мог возникнуть бесконечный цикл – программа просто зависала.&lt;/p&gt;

&lt;p&gt;Причина была в способе переноса элементов: использовалась вставка в начало списка, что в конкурентной среде могло замкнуть список само на себя. В Java 8 отказались от этого подхода в пользу вставки в конец. Бесконечные циклы ушли в прошлое. Но это не делает HashMap потокобезопасной – проблемы с потерей данных и состояниями гонки остались.&lt;/p&gt;

&lt;h2&gt;9. computeIfAbsent и merge могут бросить исключение во время выполнения&lt;/h2&gt;

&lt;p&gt;Методы &lt;code&gt;compute, computeIfAbsent&lt;/code&gt; и &lt;code&gt;merge&lt;/code&gt; удобны тем, что позволяют атомарно выполнить сложную логику вставки. Но у них есть особенность: они могут изменять мапу во время работы переданной функции.&lt;/p&gt;

&lt;p&gt;Если внутри этой функции попытаться снова изменить ту же мапу (например, вызвать &lt;code&gt;put&lt;/code&gt;), можно получить &lt;code&gt;ConcurrentModificationException&lt;/code&gt;. Это не баг, а защита от некорректного состояния. Методы стараются отследить такие ситуации и прервать выполнение, чтобы не допустить разрушения структуры данных.&lt;/p&gt;

&lt;h2&gt;10. Даже списки в Java 8 ищут быстрее&lt;/h2&gt;

&lt;p&gt;Даже если в корзине меньше восьми элементов и дерево ещё не создано, поиск работает чуть быстрее обычного последовательного перебора. В коде есть проверка: если ключи реализуют интерфейс &lt;code&gt;Comparable&lt;/code&gt;, алгоритм использует сравнение для более быстрого ветвления.&lt;/p&gt;

&lt;p&gt;Это микрооптимизация, но она хорошо иллюстрирует подход разработчиков: улучшения делаются даже там, где кажется, что они не особо нужны&lt;/p&gt;

&lt;p&gt;&lt;u&gt;​​​​​​&lt;/u&gt;​​​​​​​​​​​​Эти детали редко всплывают в повседневной работе. Но когда сталкиваешься с неожиданным поведением или пытаешься выжать из приложения максимум производительности, знание внутреннего устройства HashMap помогает быстрее понять, что идёт не так и как это исправить.&lt;br /&gt;
Исходный код HashMap открыт, и в нём можно найти ещё много интересного. Иногда полезно просто заглянуть – хотя бы ради любопытства.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Polina Napolskaya</dc:creator>
    <dc:date>2026-03-04T08:14:00Z</dc:date>
  </entry>
  <entry>
    <title>Go вместе изучать Go. Часть 6 (Заключительная)</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21222577" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21222577</id>
    <updated>2026-03-01T15:09:42Z</updated>
    <published>2026-03-01T14:54:00Z</published>
    <summary type="html">&lt;style type="text/css"&gt;article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}

article img {
width: 90%;
}

article li {
 font-size:11pt;   
}

.centered {
text-align:center;
}


article .portlet-msg-info {
color: #232323;
background-color: #f9f9f9;
border-style: dashed;
border-color: #232323;
}
&lt;/style&gt;
&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Введение&lt;/h2&gt;

&lt;p&gt;Добро пожаловать в заключительную статью нашей серии о языке программирования Go.&lt;/p&gt;

&lt;p&gt;В ней мы рассмотрим три возможности языка: работу с изображениями через пакет image, обобщения (дженерики) для написания универсального кода и конкурентность — одну из главных «визитных карточек» Go.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Работа с изображениями&lt;/h2&gt;

&lt;p&gt;Go использует интерфейсы для определения поведения. Интерфейс Image демонстрирует, как определить набор методов, которые должна реализовывать структура, чтобы считаться "изображением".&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package image

type Image interface {
    ColorModel() color.Model
    Bounds() Rectangle
    At(x, y int) color.Color
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Этот интерфейс описывает три метода: ColorModel() возвращает цветовую модель изображения, Bounds() — его границы в виде прямоугольника, а At(x, y int) — цвет пикселя по заданным координатам.&lt;/p&gt;

&lt;p&gt;Обратите внимание: возвращаемое значение Rectangle метода Bounds на самом деле является типом image.Rectangle, поскольку объявление находится внутри пакета image.&lt;/p&gt;

&lt;p&gt;Типы color.Color и color.Model также являются интерфейсами, но для простоты мы будем использовать готовые реализации color.RGBA и color.RGBAModel из пакета image/color.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"image"
)

func main() {
	// Создаем 100x100 пикселей типа RGBA
	m := image.NewRGBA(image.Rect(0, 0, 100, 100)) 
	
    // Мы можем вызвать методы, потому что *image.RGBA 
    // неявно реализует интерфейс image.Image.
	fmt.Println(m.Bounds())
	fmt.Println(m.At(0, 0).RGBA())
}
// Вывод:
// (0,0)-(100,100)
// 0 0 0 0&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;В этом примере мы создаём RGBA-изображение размером 100×100 пикселей. Функция Bounds() возвращает прямоугольник от точки (0,0) до точки (100,100). Метод At(0, 0).RGBA() возвращает четыре нуля — это значения красного, зелёного, синего и альфа-каналов для пикселя в левом верхнем углу. Нули означают полностью прозрачный чёрный цвет.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Универсальность кода: Обобщения (Generics)&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Обобщения, появившиеся в Go 1.18, позволяют писать функции и типы, которые могут работать с любым типом данных, сохраняя при этом типобезопасность.&lt;/p&gt;

&lt;h3&gt;Параметры типов&lt;/h3&gt;

&lt;p&gt;Функции в Go можно писать так, чтобы они работали с несколькими типами, используя параметры типов. Параметры типов указываются в квадратных скобках перед аргументами функции:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
func Index[T comparable](s []T, x T) int&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Это объявление означает, что s — это срез элементов любого типа T, который удовлетворяет встроенному ограничению comparable. Значение x имеет тот же тип.&lt;/p&gt;

&lt;p&gt;Ограничение comparable позволяет использовать операторы == и != для значений данного типа. В примере ниже мы используем его для сравнения значения со всеми элементами среза до нахождения совпадения:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

// Index возвращает индекс x в срезе s или -1, если элемент не найден.
func Index[T comparable](s []T, x T) int {
    for i, v := range s {
        if v == x {
            return i
        }
    }
    return -1
}

func main() {
    // Index работает со срезом целых чисел
    si := []int{10, 20, 15, -10}
    fmt.Println(Index(si, 15))

    // Index также работает со срезом строк
    ss := []string{"foo", "bar", "baz"}
    fmt.Println(Index(ss, "hello"))
}

// Вывод:
// 2
// -1&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;Функция Index универсальна: она работает и с числами, и со строками — с любым типом, поддерживающим сравнение. Компилятор сам определяет конкретный тип T на основе переданных аргументов.&lt;/p&gt;

&lt;h3&gt;Обобщённые типы&lt;/h3&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Помимо обобщённых функций, Go поддерживает обобщённые типы. Тип можно параметризовать параметром типа, что особенно полезно для реализации универсальных структур данных.&lt;/p&gt;

&lt;p&gt;Вот пример объявления типа для односвязного списка, хранящего значения любого типа, с добавленной функциональностью:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

// List представляет односвязный список, хранящий значения любого типа.
type List[T any] struct {
    next *List[T]
    val  T
}

// Push добавляет новый элемент в начало списка и возвращает новую голову.
func (l *List[T]) Push(val T) *List[T] {
    return &amp;amp;List[T]{next: l, val: val}
}

// Len возвращает длину списка.
func (l *List[T]) Len() int {
    count := 0
    for current := l; current != nil; current = current.next {
        count++
    }
    return count
}

// ToSlice преобразует список в срез.
func (l *List[T]) ToSlice() []T {
    var result []T
    for current := l; current != nil; current = current.next {
        result = append(result, current.val)
    }
    return result
}

// Find ищет элемент в списке (работает только для comparable типов).
func Find[T comparable](l *List[T], val T) bool {
    for current := l; current != nil; current = current.next {
        if current.val == val {
            return true
        }
    }
    return false
}

func main() {
    // Создаём список целых чисел
    var head *List[int]
    head = head.Push(1)
    head = head.Push(2)
    head = head.Push(3)

    fmt.Println("Длина списка:", head.Len())
    fmt.Println("Элементы:", head.ToSlice())
    fmt.Println("Содержит 2?", Find(head, 2))
    fmt.Println("Содержит 5?", Find(head, 5))
}

// Вывод:
// Длина списка: 3
// Элементы: [3 2 1]
// Содержит 2? true
// Содержит 5? false&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Обратите внимание: метод Push возвращает новую голову списка, поскольку мы добавляем элементы в начало. Функция Find объявлена отдельно с ограничением comparable, потому что не все типы поддерживают сравнение через ==.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Конкурентность&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;Горутины&lt;/h3&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Горутина — это легковесный поток, управляемый средой выполнения Go:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
go f(x, y, z)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Эта команда запускает новую горутину, выполняющую f(x, y, z). Вычисление f, x, y и z происходит в текущей горутине, а выполнение f — в новой.&lt;/p&gt;

&lt;p&gt;Горутины работают в одном адресном пространстве, поэтому доступ к общей памяти должен быть синхронизирован. Пакет sync предоставляет полезные примитивы, хотя в Go чаще используются другие механизмы — каналы.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i &amp;lt; 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;В этом примере две горутины выполняются параллельно: одна печатает «world», другая — «hello». Порядок вывода может варьироваться от запуска к запуску, поскольку горутины работают конкурентно.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
// Пример вывода (порядок может меняться):
// hello
// hello
// world
// world
// ...
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;Каналы&lt;/h3&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Каналы — это типизированные каналы связи, через которые можно отправлять и получать значения с помощью оператора &amp;lt;-:&lt;/p&gt;

&lt;p&gt;Каналы (chan) служат для безопасного обмена данными между горутинами.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
ch &amp;lt;- v    // Отправить v в канал ch.
v := &amp;lt;-ch  // Получить значение из ch и присвоить его v.&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Данные «текут» в направлении стрелки. Как и карты (maps) и срезы, каналы нужно создавать перед использованием:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
ch := make(chan int)&lt;/pre&gt;

&lt;p&gt;По умолчанию операции отправки и получения блокируются, пока другая сторона не будет готова. Это позволяет горутинам синхронизироваться без явных блокировок.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c &amp;lt;- sum // отправить сумму в канал c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := &amp;lt;-c, &amp;lt;-c // получить из канала c

    fmt.Println(x, y, x+y)
}

// Вывод: -5 17 12&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Здесь работа по суммированию распределяется между двумя горутинами. Каждая считает сумму своей половины среза и отправляет результат в канал. Главная горутина получает оба значения и складывает их.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;Буферизованные каналы&lt;/h3&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Каналы могут быть буферизованными. Размер буфера указывается вторым аргументом make.&lt;/p&gt;

&lt;p&gt;Буферизованные каналы позволяют отправить несколько значений, не блокируя отправителя, пока буфер не заполнится.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
ch := make(chan int, 100)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Отправка в буферизованный канал блокируется только когда буфер заполнен. Получение блокируется, когда буфер пуст.&lt;/p&gt;

&lt;p&gt;Вот что происходит при переполнении буфера:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch &amp;lt;- 1
    ch &amp;lt;- 2
    // ch &amp;lt;- 3 // Эта строка вызовет deadlock!
    
    fmt.Println(&amp;lt;-ch)
    fmt.Println(&amp;lt;-ch)
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Если раскомментировать строку ch &amp;lt;- 3, программа зависнет (deadlock), потому что буфер размером 2 уже заполнен, и отправка будет ждать, пока кто-то не прочитает из канала. Но читать некому — главная горутина заблокирована на отправке. Результат — взаимная блокировка:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
fatal error: all goroutines are asleep - deadlock!&lt;/pre&gt;

&lt;p&gt;Это важный урок: размер буфера нужно выбирать с учётом того, сколько значений может быть отправлено до того, как получатель их обработает.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;Завершение передачи данных (Range и Close)&lt;/h3&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Отправитель может закрыть канал, чтобы сигнализировать, что больше значений не будет. Получатель может проверить, закрыт ли канал:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
v, ok := &amp;lt;-ch&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Значение ok будет false, если канал закрыт и пуст.&lt;/p&gt;

&lt;p&gt;Цикл for i := range c получает значения из канала до его закрытия.&lt;/p&gt;

&lt;p&gt;Важно: закрывать канал должен только отправитель, никогда — получатель. Отправка в закрытый канал вызовет панику. При этом каналы не похожи на файлы — их обычно не нужно закрывать. Закрытие необходимо только когда получателю нужно знать, что значений больше не будет.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i &amp;lt; n; i++ {
        c &amp;lt;- x
        x, y = y, x+y
    }
    close(c) // Сигнал об окончании
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)

	// Цикл range автоматически завершится, когда fibonacci вызовет close(c).
    for i := range c {
        fmt.Println(i)
    }
}

// Вывод: 0 1 1 2 3 5 8 13 21 34&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;Управляемый выбор: select&lt;/h3&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Оператор select позволяет горутине ожидать готовности нескольких каналов связи. Он выбирает первый готовый случай, а если их несколько — выбирает случайным образом.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c &amp;lt;- x:
            x, y = y, x+y
        case &amp;lt;-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i &amp;lt; 10; i++ {
            fmt.Println(&amp;lt;-c)
        }
        quit &amp;lt;- 0
    }()
    fibonacci(c, quit)
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;select блокируется до тех пор, пока один из его case-ов не сможет выполниться. Если готовы несколько — выбирается случайный.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;Default в Select&lt;/h3&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Ветка default выполняется, если ни один другой case не готов. Это позволяет выполнять неблокирующие операции:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
select {
case i := &amp;lt;-c:
    // Данные пришли
default:
    //Данные не пришли немедленно, продолжаем работу
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;Взаимное исключение: sync.Mutex&lt;/h3&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Каналы отлично подходят для коммуникации между горутинами. Но если нам нужно просто гарантировать, что только одна горутина имеет доступ к переменной в данный момент (общение через каналы в данном случае просто напросто избыточно)?&lt;/p&gt;

&lt;p&gt;Для этого существует взаимное исключение (mutex). Стандартная библиотека Go предоставляет sync.Mutex с двумя методами: Lock и Unlock.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
    "fmt"
    "sync"
    "time"
)

// SafeCounter безопасен для конкурентного использования.
type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

// Inc увеличивает счётчик для заданного ключа.
func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    c.v[key]++
    c.mu.Unlock()
}

// Value возвращает текущее значение счётчика.
func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i &amp;lt; 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}

// Вывод: 1000&lt;/pre&gt;

&lt;p&gt;Без мьютекса 1000 горутин, одновременно изменяющих карту, вызвали бы гонку данных (data race) и непредсказуемое поведение. Мьютекс гарантирует, что в каждый момент времени только одна горутина работает с картой.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Обратите внимание на использование defer c.mu.Unlock() в методе Value — это гарантирует разблокировку даже при ранних возвратах или панике.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Заключение&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Из этой статьи мы научились работать с несколькими важными аспектами Go. Мы узнали, как использовать пакет image для создания и манипулирования изображениями через интерфейс Image. Освоили дженерики — мощный механизм для написания универсального, переиспользуемого кода с параметрами типов и ограничениями вроде comparable и any. Погрузились в мир конкурентности Go: изучили горутины как легковесные потоки, каналы как средство безопасной коммуникации, оператор select для работы с несколькими каналами одновременно и мьютексы для защиты общих данных.&lt;/p&gt;

&lt;p&gt;Эта статья завершает нашу серию материалов о языке Go. Мы прошли путь от основ синтаксиса до продвинутых возможностей языка, и теперь у вас есть некоторая база для написания эффективных, безопасных и элегантных программ на Go.&lt;/p&gt;

&lt;p&gt;Успехов&amp;nbsp;в ваших проектах!&lt;/p&gt;</summary>
    <dc:creator>Romo Fedoroff</dc:creator>
    <dc:date>2026-03-01T14:54:00Z</dc:date>
  </entry>
  <entry>
    <title>iOS разработка: Как обнулить счетчик уведомлений в приложении</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21221410" />
    <author>
      <name>Никита Рогаленко</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21221410</id>
    <updated>2026-02-26T13:16:20Z</updated>
    <published>2026-02-26T13:15:00Z</published>
    <summary type="html">&lt;p style="text-align: justify;"&gt;Хотя мобильная разработка и не является основным профилем нашей компании, порой среди требований к корпоративному порталу оказывается его доступность с мобильных устройств. В связи с этим иногда приходится браться за нетипичные для нас задачи в виде разработки iOS и Android приложений. Однако оставим задачу систематического изложения основ мобильной разработки на другой раз. В этой заметке кратко зафиксируем, как наиболее просто решить проблему с красным значком с количеством уведомлений у иконки iOS-приложения, который не пропадает при переходе в программу. Речь идет о подобном красном кружке:&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;img alt="Display a badge on the app icon - Progressive web apps | MDN" class="sFlh5c FyHeAf iPVvYb" jsaction="" jsname="kn3ccd" src="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Display_badge_on_app_icon/mail-badge-ios.png" style="max-width: 1170px; height: 174px; margin: 10px 0px; width: 574px;" /&gt;​​​​​​​&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Зачастую происходит так, что при открытии приложения счетчик непрочитанных уведомлений не исчезает, а остается неизменным. Особенно часто такое возникает в случае, если переход происходит не по клику на уведомление в "Центре уведомлений", а непосредственно через экранное меню айфона.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Если мы хотим, чтобы все новые уведомления в приложении отмечались как прочитанные при переходе в приложение, то ответ кроется в модификации класса&amp;nbsp;SceneDelegate.&amp;nbsp;SceneDelegate - это класс в Swift, который отвечает за управление жизненным циклом конкретного экземпляра пользовательского интерфейса (сцены). Класс создает объект "контейнер" для элементов UIWindow, управляет состояниями приложения, размещением контента на странице, обработкой касаний разных элементов, переходами между фоновым и активным режимами.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Отличие SceneDelegate&amp;nbsp;от AppDelegate в том, что последний предназначен для глобальной конфигурации всего приложения в целом. Там же происходит и регистрация Push-уведомлений, инициализация Firebase или иных уведомлений. Сейчас же нам нужен&amp;nbsp;SceneDelegate, поскольку именно он обрабатывает событие открытия "сцены" приложения, при переходе в которую мы хотим обнулять счетчик уведомлений.&amp;nbsp;Метод, который нам нужен для обработки события - sceneDidBecomeActive(_:). Он вызывается, когда сцена стала активной и готова к взаимодействию с пользователем.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Кусок кода на языке Swift, чтобы достичь поставленной задачи:&lt;/p&gt;

&lt;pre class="brush:as3;"&gt;
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else {
            return
        }
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
            if #available(iOS 17.0, *) {
                UNUserNotificationCenter.current().setBadgeCount(0) { error in
                    if let error = error { print("sceneDidBecomeActive ERROR: \(error)") }
                }
            } else {
                UIApplication.shared.applicationIconBadgeNumber = 0
            }
            UNUserNotificationCenter.current().removeAllDeliveredNotifications()
        }

}&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;Для управления уведомлениями в iOS используется класс&amp;nbsp;UNUserNotificationCenter. Во фрагменте кода выше мы используем его методы для сброса счетчика уведомлений на иконке приложения в ноль (метод&amp;nbsp;setBadgeCount), а также для удаления уведомлений, пришедших от нашего приложения, в ленте "Центра уведомлений" (метод&amp;nbsp;removeAllDeliveredNotifications).&amp;nbsp;&lt;span aria-level="2" class="VndcI veK2kb" role="heading"&gt;&lt;span&gt;Метод удаления через UIApplication.shared.&lt;/span&gt;&lt;/span&gt;​​​​​​​applicationIconBadgeNumber является устаревшим (для версий до iOS 17)&lt;/p&gt;</summary>
    <dc:creator>Никита Рогаленко</dc:creator>
    <dc:date>2026-02-26T13:15:00Z</dc:date>
  </entry>
  <entry>
    <title>Keycloak: Identity Broker и настройка realm-to-realm брокеринга</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21216208" />
    <author>
      <name>Maxim Kalabukhov</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21216208</id>
    <updated>2026-02-12T07:37:16Z</updated>
    <published>2026-02-12T07:24:00Z</published>
    <summary type="html">&lt;p&gt;Добрый день!&lt;/p&gt;

&lt;p&gt;Сегодня я хочу поговорить о механизме &lt;strong&gt;Identity Brokering&lt;/strong&gt; внутри Keycloak, а точнее о возможности использовать один realm как Identity Provider для другого. Звучит просто, но на практике нюансов хватает, поэтому давайте разберёмся по порядку.&lt;/p&gt;

&lt;h2&gt;Что такое Identity Brokering&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Identity Broker&lt;/strong&gt; - это посредник, который устанавливает доверительные отношения между сервис-провайдером (приложением, которому нужна аутентификация) и одним или несколькими Identity Provider-ами (источниками учётных данных). В контексте Keycloak это означает, что один realm может делегировать аутентификацию другому realm-у - как внутри одного инстанса Keycloak, так и между разными серверами.&lt;/p&gt;

&lt;p&gt;Суть проблемы: представьте, что в вашей организации несколько подразделений, каждое со своим realm-ом в Keycloak. У отдела разработки свой realm &lt;code&gt;dev-realm&lt;/code&gt; с разработчиками, у HR - свой &lt;code&gt;hr-realm&lt;/code&gt; с кадровиками. И вот появляется корпоративный портал, которому нужно пускать и тех, и других. Заводить всех пользователей заново? Синхронизировать базы руками? Нет, спасибо. Вот тут-то и приходит на помощь Identity Brokering.&lt;/p&gt;

&lt;p&gt;Keycloak в роли брокера перенаправляет пользователя на страницу логина того realm-а, где хранятся его учётные данные, получает обратно токен и на его основе создаёт (или находит) локального пользователя в своём realm-е.&lt;/p&gt;

&lt;h2&gt;Подготовка окружения&lt;/h2&gt;

&lt;p&gt;Для демонстрации нам потребуется один инстанс Keycloak.&lt;br /&gt;
Мы создадим два realm-а:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;idp-realm&lt;/strong&gt; - realm, выступающий в роли Identity Provider. Здесь живут пользователи.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;sp-realm&lt;/strong&gt; - realm, выступающий в роли Service Provider (потребитель). Сюда будут «приходить» пользователи через брокеринг.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Запускаем Keycloak через Docker Compose. Создаём файл &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;services:
  keycloak:
    image: quay.io/keycloak/keycloak:25.0.0
    command: start-dev
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    ports:
      - "8080:8080"&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Поднимаем:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Теперь можно заходить в админку &lt;code&gt;http://localhost:8080&lt;/code&gt; и авторизоваться.&lt;/p&gt;

&lt;h2&gt;Настройка realm-провайдера (IDP realm)&lt;/h2&gt;

&lt;p&gt;Начнём с realm-а, который будет отдавать пользователей. Создаём новый realm с именем &lt;code&gt;idp-realm&lt;/code&gt;. В админке нажимаем на выпадающий список realm-ов в левом верхнем углу -&amp;gt; &lt;strong&gt;Create realm&lt;/strong&gt;. Указываем имя &lt;code&gt;idp-realm&lt;/code&gt; и жмём &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Теперь, чтобы &lt;code&gt;sp-realm&lt;/code&gt; мог аутентифицировать пользователей через &lt;code&gt;idp-realm&lt;/code&gt;, в &lt;code&gt;idp-realm&lt;/code&gt; необходимо создать клиент (Client), который будет представлять собой наш &lt;code&gt;sp-realm&lt;/code&gt;.&lt;br /&gt;
Переходим в &lt;strong&gt;Clients&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Create client&lt;/strong&gt; и заполняем:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;Client ID:            sp-realm-broker
Client type:          OpenID Connect
Valid redirect URIs:  http://localhost:8080/realms/sp-realm/broker/idp-realm-oidc/endpoint/*&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;В поле &lt;code&gt;Valid redirect URIs&lt;/code&gt; мы указываем callback-URL того realm-а, который будет потребителем. Формат URL-а: &lt;code&gt;{keycloak-base}/realms/{consumer-realm}/broker/{identity-provider-alias}/endpoint/*&lt;/code&gt;. Алиас &lt;code&gt;idp-realm-oidc&lt;/code&gt; - это имя, которое мы дадим Identity Provider-у в &lt;code&gt;sp-realm&lt;/code&gt; на следующем этапе.&lt;/p&gt;

&lt;p&gt;В настройках клиента включаем &lt;strong&gt;Client authentication&lt;/strong&gt; (т.е. клиент будет конфиденциальным), после сохранения переходим на вкладку &lt;strong&gt;Credentials&lt;/strong&gt; и копируем &lt;strong&gt;Client secret&lt;/strong&gt; - он нам скоро понадобится.&lt;/p&gt;

&lt;p&gt;Для проверки работы брокеринга создаём пользователя:&lt;br /&gt;
Переходим в &lt;strong&gt;Users&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Add user&lt;/strong&gt;:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;Username:   testuser
Email:      testuser@example.com
First Name: Test
Last Name:  User&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;После создания открываем вкладку &lt;strong&gt;Credentials&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Set password&lt;/strong&gt;, задаём пароль и снимаем галку &lt;strong&gt;Temporary&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;Настройка realm-потребителя (SP realm)&lt;/h2&gt;

&lt;p&gt;Теперь настроим realm, который будет принимать пользователей из &lt;code&gt;idp-realm&lt;/code&gt;.&lt;br /&gt;
Аналогично создаём realm с именем &lt;code&gt;sp-realm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Вот тут начинается самое интересное. Переходим в &lt;code&gt;sp-realm&lt;/code&gt; -&amp;gt; &lt;strong&gt;Identity providers&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Keycloak OpenID Connect&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;На наше счастье, Keycloak умеет автоматически подтягивать конфигурацию через Discovery URL. Заполняем поля:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;Alias:           idp-realm-oidc
Discovery URL:   http://localhost:8080/realms/idp-realm/.well-known/openid-configuration
Client ID:       sp-realm-broker
Client Secret:   &amp;lt;сИкрет, скопированный на предыдущем этапе&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;После указания &lt;strong&gt;Discovery URL&lt;/strong&gt; и нажатия кнопки обновления Keycloak автоматически заполнит все endpoint-ы и можно радоваться жизни.&lt;/p&gt;

&lt;p&gt;Дополнительные настройки, на которые стоит обратить внимание:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Store tokens&lt;/strong&gt; - если включить, Keycloak будет хранить токены, полученные от IDP. Полезно, если вам нужно обращаться к API от имени пользователя.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Trust emails&lt;/strong&gt; - если в IDP realm-е email пользователя уже подтверждён, можно включить эту опцию, чтобы не требовать повторного подтверждения.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Sync mode&lt;/strong&gt; - определяет, как обновлять данные пользователя при повторном логине. &lt;code&gt;Import&lt;/code&gt; - только при первом входе, &lt;code&gt;Force&lt;/code&gt; - каждый раз.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Чтобы проверить работу брокеринга, создадим простой клиент в &lt;code&gt;sp-realm&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;Client ID:           test-app
Client type:         OpenID Connect
Valid redirect URIs:  http://localhost:8080/realms/sp-realm/account/*&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Можно также воспользоваться встроенным Account Console, который доступен по адресу &lt;code&gt;http://localhost:8080/realms/sp-realm/account&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;Проверяем работу&lt;/h2&gt;

&lt;p&gt;Открываем в браузере:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;http://localhost:8080/realms/sp-realm/account&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;На странице логина &lt;code&gt;sp-realm&lt;/code&gt; появится кнопка &lt;strong&gt;IDP realm Login&lt;/strong&gt; (или то имя, которое вы указали в Display name). Нажимаем на неё - нас перенаправит на страницу логина &lt;code&gt;idp-realm&lt;/code&gt;. Вводим креды нашего &lt;code&gt;testuser&lt;/code&gt;, и после успешной аутентификации нас вернёт обратно в &lt;code&gt;sp-realm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21214588/Pasted+image+20260208150740.png/6ba7d0ff-816a-1b6f-295b-2eadf4b082c1?imagePreview=1" /&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;При первом входе Keycloak предложит пользователю подтвердить свой профиль (если включена соответствующая опция) и создаст локальную запись в &lt;code&gt;sp-realm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21214588/Pasted+image+20260208150845.png/f431053e-6cb1-16db-ed31-9b460a8e3741?imagePreview=1" /&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;Маппинг атрибутов и ролей&lt;/h2&gt;

&lt;p&gt;По умолчанию Keycloak прокидывает базовые атрибуты - username, email, first name, last name. Но что, если нам нужно прокинуть кастомные атрибуты или роли?&lt;/p&gt;

&lt;h3&gt;Маппинг атрибутов&lt;/h3&gt;

&lt;p&gt;Переходим в &lt;code&gt;sp-realm&lt;/code&gt; -&amp;gt; &lt;strong&gt;Identity providers&lt;/strong&gt; -&amp;gt; &lt;code&gt;idp-realm-oidc&lt;/code&gt; -&amp;gt; вкладка &lt;strong&gt;Mappers&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Add mapper&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Допустим, в &lt;code&gt;idp-realm&lt;/code&gt; у пользователя есть атрибут &lt;code&gt;department&lt;/code&gt;. Создаём маппер:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;Name:                    Department Mapper
Sync mode override:      Inherit
Mapper type:             Attribute Importer
Claim:                   department
User Attribute Name:     department&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Теперь при каждом логине значение клейма &lt;code&gt;department&lt;/code&gt; из токена IDP будет записываться в атрибут пользователя в &lt;code&gt;sp-realm&lt;/code&gt;. Правда есть нюанс - чтобы кастомный атрибут попадал в токен IDP realm-а, нужно создать &lt;strong&gt;Client scope&lt;/strong&gt; в &lt;code&gt;idp-realm&lt;/code&gt; с соответствующим маппером типа &lt;strong&gt;User Attribute&lt;/strong&gt;, и назначить этот scope клиенту &lt;code&gt;sp-realm-broker&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;Маппинг ролей&lt;/h3&gt;

&lt;p&gt;Для ролей создаём маппер типа &lt;strong&gt;Claim to Role&lt;/strong&gt;:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;Name:             Role Mapper - Manager
Sync mode:        Inherit
Mapper type:      Claim to Role
Claim:            realm_access.roles
Claim Value:      manager
Role:             manager  (предварительно создайте эту роль в sp-realm)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Теперь пользователи с ролью &lt;code&gt;manager&lt;/code&gt; в &lt;code&gt;idp-realm&lt;/code&gt; автоматически получат роль &lt;code&gt;manager&lt;/code&gt; в &lt;code&gt;sp-realm&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;Первый логин и стратегии обнаружения пользователей&lt;/h2&gt;

&lt;p&gt;Когда пользователь впервые приходит через брокеринг, Keycloak должен решить: создать нового пользователя или привязать к существующему? За это отвечает настройка &lt;strong&gt;First login flow&lt;/strong&gt; в конфигурации Identity Provider-а.&lt;/p&gt;

&lt;p&gt;По умолчанию используется flow &lt;code&gt;first broker login&lt;/code&gt;, который включает в себя:&lt;/p&gt;

&lt;ol&gt;
	&lt;li&gt;&lt;strong&gt;Review Profile&lt;/strong&gt; - пользователю предлагают проверить свои данные.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Create User If Unique&lt;/strong&gt; - если пользователя с таким email/username нет, он создаётся автоматически.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Confirm Link Existing Account&lt;/strong&gt; - если пользователь уже существует, предлагается привязка аккаунтов.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Для корпоративного использования часто имеет смысл кастомизировать этот flow. Например, если вы уверены, что email уникален и доверяете IDP, можно убрать шаг Review Profile и автоматически линковать аккаунты без подтверждения.&lt;/p&gt;

&lt;p&gt;Для этого:&lt;/p&gt;

&lt;ol&gt;
	&lt;li&gt;Переходим в &lt;strong&gt;Authentication&lt;/strong&gt; -&amp;gt; дублируем flow &lt;code&gt;first broker login&lt;/code&gt;.&lt;/li&gt;
	&lt;li&gt;В нашей копии удаляем или отключаем &lt;code&gt;Review Profile&lt;/code&gt;.&lt;/li&gt;
	&lt;li&gt;Настраиваем &lt;code&gt;Automatically set existing user&lt;/code&gt; вместо ручного подтверждения.&lt;/li&gt;
	&lt;li&gt;Справа сверху в выпадающем списке назначаем наш кастомный flow через &lt;strong&gt;Bind flow&lt;/strong&gt; в настройках Identity Provider-а в поле &lt;strong&gt;First broker login flow&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;На этом всё. Мы рассмотрели полный цикл настройки realm-to-realm Identity Brokering-а в Keycloak: от создания realm-ов и клиентов до маппинга атрибутов и кастомизации первого логина. Механизм мощный и гибкий - позволяет строить федеративную аутентификацию без дублирования пользовательских баз и без сторонних решений.&lt;/p&gt;

&lt;p&gt;Если тема Keycloak вам интересна, рекомендую заглянуть в официальную документацию по &lt;a href="https://www.keycloak.org/docs/latest/server_admin/#_identity_broker" rel="noopener noreferrer" target="_blank"&gt;Server Administration Guide&lt;/a&gt; - там описаны продвинутые сценарии, включая брокеринг через SAML и Social Login провайдеры.&lt;/p&gt;</summary>
    <dc:creator>Maxim Kalabukhov</dc:creator>
    <dc:date>2026-02-12T07:24:00Z</dc:date>
  </entry>
  <entry>
    <title>Будущее программирования: четыре смелые идеи из 60-х, которые меняют наш цифровой мир сегодня</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21213820" />
    <author>
      <name>Polina Napolskaya</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21213820</id>
    <updated>2026-02-06T13:45:15Z</updated>
    <published>2026-02-06T12:53:00Z</published>
    <summary type="html">&lt;p&gt;В 60-е и 70-е годы прошлого века, когда компьютерная наука только зарождалась, в ней не было правил. Не было стандартов, догм или «единственно верных» подходов. Были только чистые листы и смелые умы, готовые пробовать всё, что приходило в голову. Именно в этом творческом хаосе родились концепции, которые до сих пор определяют траекторию развития программирования.&lt;/p&gt;

&lt;h2&gt;Когда незнание было преимуществом&lt;/h2&gt;

&lt;p&gt;Современному разработчику, окружённому фреймворками, паттернами и best practices, трудно представить то время. Не было React, не было Kotlin, не было даже объектно-ориентированного программирования в его нынешнем виде. И именно эта свобода от «как надо» породила самые революционные идеи.&lt;/p&gt;

&lt;p&gt;Автор выступления &lt;a href="https://youtu.be/8pTEmbeENF4?si=Vzpdqx2-nfNqubp_"&gt;«The Future of Programming»&lt;/a&gt; Виктор Брет выделяет четыре такие идеи, каждая из которых казалась фантастикой, но сегодня находит своё воплощение в самых передовых технологиях.&lt;/p&gt;

&lt;h2&gt;Рисовать, а не кодить: прямая манипуляция структурами данных&lt;/h2&gt;

&lt;p&gt;1963 год. Айван Сазерленд демонстрирует Sketchpad -- систему, где вы рисуете линии световым пером прямо на экране, а компьютер не просто сохраняет картинку, а понимает её как структуру данных. Вы задаёте две точки -- получаете отрезок. Меняете одну точку -- отрезок перерисовывается автоматически.&lt;/p&gt;

&lt;p&gt;Тогда это было чудо. Сегодня -- обыденность. Системы автоматизированного проектирования (CAD) вроде AutoCAD, Figma, инструменты для создания интерфейсов позволяют нам работать не с кодом, а с визуальными объектами. Современный дизайнер или инженер абстрагирован от битов и байтов -- он манипулирует понятными сущностями: кнопками, формами, деталями механизмов.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Суть идеи&lt;/strong&gt;: замена написания инструкций на прямое взаимодействие с данными.&lt;/p&gt;

&lt;h2&gt;Говорить «что», а не «как»: цели вместо процедур&lt;/h2&gt;

&lt;p&gt;Традиционное программирование -- это рецепт. Шаг 1, шаг 2, шаг 3... Но что если вместо алгоритма описывать только желаемый результат и ограничения?&lt;/p&gt;

&lt;p&gt;В 60-е это была теория. Сегодня -- это искусственный интеллект и машинное обучение. Мы не пишем код для распознавания лиц -- мы показываем нейросети тысячи изображений и говорим: «научись находить лица». GitHub Copilot не исполняет наш алгоритм -- он, анализируя контекст, генерирует код, который решает поставленную задачу.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Программирование эволюционирует от написания инструкций к формулированию задач&lt;/strong&gt;. Декларативный подход к написанию кода становится все более и более популярным, а prompt engineering становится новой грамотностью разработчика.&lt;/p&gt;

&lt;h2&gt;Видеть, а не читать: пространственное представление кода&lt;/h2&gt;

&lt;p&gt;Текст -- линейный и последовательный. Но многие системы -- нелинейны и многомерны. Почему бы не представлять программу не как список строк, а как схему, граф, диаграмму?&lt;/p&gt;

&lt;p&gt;Smalltalk в 70-х предложил мир объектов, которые можно «видеть» и с которыми можно «разговаривать». Сегодня LabVIEW даёт инженерам инструмент, где программа собирается из графических блоков, как конструктор. No-code/low-code платформы позволяют создавать бизнес-логику через перетаскивание элементов.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Визуальное программирование&lt;/strong&gt; делает технологии доступнее. Оно не заменит традиционное кодирование для сложных систем, но станет мостом для специалистов из других областей, которым нужно решать алгоритмические задачи.&lt;/p&gt;

&lt;h2&gt;Истинный параллелизм&lt;/h2&gt;

&lt;p&gt;Архитектура фон Неймана, основа всей современной вычислительной техники, по сути, очередь: одна инструкция за другой. Да, мы научились хитрить -- появились многопоточность, superscalar, GPU, распределённые системы… Но это всё ещё параллелизм, построенный на общей памяти и блокировках.&lt;/p&gt;

&lt;p&gt;Однако существуют концепции, в свою очередь подразумевающие принципиально новый уровень параллелизма, где процессы взаимодействуют друг с другом не через общую память, используя потоки и блокировки, а напрямую, и каждый процесс может реагировать на информацию, полученную от другого процесса. Эта концепция может быть представлена моделью акторов. Программы для устройств, использующих такой уровень параллелизма, сильно бы отличалось от тех, которые используют общую память. Но такой подход обладает гораздо большим потенциалом с точки зрения производительности.&lt;/p&gt;

&lt;h2&gt;За горизонтом: квантовый скачок&lt;/h2&gt;

&lt;p&gt;К идеям Брета хочется добавить ещё один, принципиально новый рубеж -- квантовые вычисления. Здесь программист должен мыслить не битами (0 или 1), а кубитами, которые могут быть и 0, и 1 одновременно (суперпозиция). Нужно учитывать квантовую запутанность и вероятностную природу результатов.&lt;/p&gt;

&lt;p&gt;Это программирование, где разработчик -- ещё и физик. Где отладка -- это искусство, потому что любое измерение меняет состояние системы. Такие компании, как Microsoft, уже создают топологические кубиты для стабильных квантовых систем. Пока это узкоспециализированные инструменты, но они открывают двери в мир, где решаются задачи, невозможные для классических компьютеров.&lt;/p&gt;

&lt;h2&gt;Самая опасная мысль&lt;/h2&gt;

&lt;p&gt;Ключевой посыл выступления Виктора Брета удивительно прост и глубок:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Самая опасная мысль для творческого человека -- думать, что ты знаешь, что делаешь.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;В 60-е не знали. Поэтому изобретали. Сегодня, погружённые в рутину спринтов, техдолга и бесконечных апдейтов фреймворков, мы рискуем забыть этот дух первооткрывательства.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Будущее программирования рождается не там, где следуют стандартам, а там, где их нарушают&lt;/strong&gt;. Не там, где оптимизируют существующее, а там, где представляют радикально иное. Возможно, следующая революционная идея уже ждёт своего часа -- не в исследовательском центре гиганта технологий, а в голове того, кто ещё не научился «правильно» мыслить. Кто не боится задать наивный вопрос: «А почему бы не сделать иначе?».&lt;/p&gt;

&lt;p&gt;Именно этому духу -- духу 60-х, духу творческого незнания -- нам стоит поучиться. Ведь все сегодняшние «стандарты» когда-то были самыми смелыми идеями на свете.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Polina Napolskaya</dc:creator>
    <dc:date>2026-02-06T12:53:00Z</dc:date>
  </entry>
  <entry>
    <title>Как психология в дизайне управляет нашим выбором</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21212798" />
    <author>
      <name>Алексей Кондратьев</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21212798</id>
    <updated>2026-02-03T16:20:33Z</updated>
    <published>2026-02-03T16:05:00Z</published>
    <summary type="html">&lt;p&gt;Мы часто думаем о дизайне как о красоте, стиле или удобстве. Но на более глубоком уровне эффективный дизайн — это тонкая и мощная форма психологического воздействия. Он не просто украшает интерфейс, он направляет наше внимание, формирует восприятие, упрощает сложное и, в конечном счете, подталкивает нас к определенным решениям — от нажатия на кнопку до совершения покупки.&lt;br /&gt;
&lt;br /&gt;
Эта статья — путеводитель по ключевым психологическим принципам, которые лежат в основе работы выдающегося дизайна. Мы рассмотрим два основных пласта: когнитивную психологию (как мы воспринимаем и обрабатываем информацию) и поведенческую экономику (как мы принимаем решения, часто иррациональные).&lt;/p&gt;

&lt;h2&gt;&lt;b id="docs-internal-guid-1fbd33f2-7fff-a98b-ece3-55cfcba18067"&gt;Когнитивная психология — дизайн для мозга&lt;/b&gt;&lt;/h2&gt;

&lt;p&gt;Мозг — персонаж неглупый и со своими принципами. Он стремится к экономии энергии, поэтому использует когнитивные шаблоны для быстрой интерпретации входящей информации. Хороший дизайн говорит на языке этих шаблонов.&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-c55ef27e-7fff-0f1a-be1b-0a1ededd42bf"&gt;1. Законы гештальта.&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Гештальт (форма на немецком языке) — это группа принципов визуального восприятия, разработанная немецкими психологами в 1920-х годах. Он основан на теории, что «организованное целое воспринимается как большее, чем сумма его частей». Наш мозг автоматически группирует элементы по определенным правилам, чтобы увидеть структуру.&lt;/p&gt;

&lt;ul&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-b3dee676-7fff-5cfc-9bc3-457e835c61db"&gt;Близость (Proximity): &lt;/b&gt;Расположенные близко друг к другу элементы воспринимаются как группа. В дизайне: Это основа компоновки форм. Поле ввода и его подпись должны быть ближе друг к другу, чем к следующему полю. Так вы группируете связанную информацию без лишних линий.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-b3dee676-7fff-5cfc-9bc3-457e835c61db"&gt;Схожесть (Similarity): &lt;/b&gt;Похожие элементы (по цвету, форме, размеру) воспринимаются как группа. В дизайне: Все кнопки «Купить» одного цвета, все ссылки подчеркнуты. Это создает визуальные паттерны и систему.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-b3dee676-7fff-5cfc-9bc3-457e835c61db"&gt;Замкнутость (Closure): &lt;/b&gt;Мозг «дорисовывает» недостающие части, чтобы завершить знакомую фигуру. В дизайне: Логотип WWF (панда из незавершенных линий), использование негативного пространства. Позволяет создавать простые, но запоминающиеся образы.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-b3dee676-7fff-5cfc-9bc3-457e835c61db"&gt;Общая зона (Common Region): &lt;/b&gt;Элементы в одной замкнутой области воспринимаются как группа. В дизайне: Карточки товаров, всплывающие окна (модальные окна), разделы с фоновой заливкой. Мощный инструмент для выделения блоков информации.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;На практике: Законы гештальта нужно использовать осознанно. Они — главный инструмент для создания ясной визуальной иерархии без лишнего «визуального шума».&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-65a16f45-7fff-1a2b-7442-6702258518d5"&gt;2. Теория цвета.&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Цвет — это не эстетика, а коммуникация. Он вызывает мгновенные эмоциональные и физиологические реакции, обходящие сознательный анализ.&lt;/p&gt;

&lt;ul&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-1f9d6c61-7fff-6634-12dd-bfae7e5a2353"&gt;Психология и культурный код: &lt;/b&gt;Красный сигнализирует об опасности, срочности или страсти (отсюда кнопки «Удалить» и «Купить»). Зеленый ассоциируется с природой, безопасностью и разрешением («Подтвердить»). Синий внушает доверие и стабильность (почему его любят банки и соцсети). Но важно: Культурный контекст меняет значение (белый — цвет свадьбы на Западе и траура в некоторых азиатских культурах).&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-1f9d6c61-7fff-6634-12dd-bfae7e5a2353"&gt;Функциональность и доступность: &lt;/b&gt;Контраст текста и фона критически важен для читаемости и соответствия стандартам доступности (WCAG). Цвет не должен быть единственным способом передачи информации (например, «обязательные поля отмечены красным» — нужно и символом *).&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;На практике: Выбирайте цветовую палитру, исходя из задачи бренда (доверие, возбуждение, спокойствие) и функциональных требований, а не личных предпочтений. Тестируйте контраст и учитывайте цветовую слепоту.&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-e4b55d34-7fff-d21b-ce3b-8423813b9e1f"&gt;3. Ментальные модели: встреча ожиданий и реальности&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Ментальная модель — это внутреннее представление пользователя о том, как что-то работает, основанное на прошлом опыте. Например, мы ожидаем, что корзина покупок будет в правом верхнем углу, а логотип в левом верхнем углу будет вести на главную.&lt;/p&gt;

&lt;ul&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7b8d8a04-7fff-0bdb-f75d-2656d805a45d"&gt;Конфликт моделей: &lt;/b&gt;Когда дизайн продукта (концептуальная модель) противоречит ожиданиям пользователя (ментальной модели), возникает когнитивная нагрузка, разочарование и ошибки. Пример: нестандартная иконка «Сохранить», которую невозможно узнать.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7b8d8a04-7fff-0bdb-f75d-2656d805a45d"&gt;Использование знакомых паттернов: &lt;/b&gt;Следование общепринятым UX-паттернам (навигация, формы, взаимодействия) — это уважение к ментальным моделям пользователя. Это снижает порог входа и делает интерфейс предсказуемым.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;На практике: Проводите юзабилити-тесты, чтобы выявить ментальные модели вашей аудитории. Используйте привычные метафоры (рабочий стол, папки, книги). Инновации вводите осторожно и тогда, когда они дают очевидную выгоду.&lt;/p&gt;

&lt;h2&gt;&lt;b id="docs-internal-guid-e95e12eb-7fff-58c3-eba8-3b6d3b81c027"&gt;Поведенческая экономика — дизайн для иррационального «Я»&lt;/b&gt;&lt;/h2&gt;

&lt;p&gt;Нобелевский лауреат Даниэль Канеман доказал: человек — не рациональный «хомо экономикус». Нами управляют систематические ошибки мышления (когнитивные искажения). Дизайн, который их учитывает, становится невероятно убедительным.&lt;/p&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-1ca3ad2c-7fff-9013-5966-3006942647e6"&gt;Принципы «подталкивания» (Nudges) в дизайне&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Nudge («подталкивание») — это мягкое изменение среды выбора, которое предсказуемо направляет человека к лучшему решению, не ограничивая его свободы.&lt;/p&gt;

&lt;ol&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;Эффект владения (Endowment Effect) и Статус-кво:&lt;/b&gt; Мы переоцениваем то, что уже имеем, и предпочитаем оставлять всё как есть.&lt;/p&gt;

	&lt;ul&gt;
		&lt;li aria-level="2" dir="ltr"&gt;
		&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;В дизайне: &lt;/b&gt;Бесплатный пробный период. После того как пользователь «присвоил» сервис на 14 дней, ему психологически сложнее от него отказаться. Настройки по умолчанию (например, опция «зеленого» тарифа) часто остаются неизменными.&lt;/p&gt;
		&lt;/li&gt;
	&lt;/ul&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;Социальное доказательство (Social Proof): &lt;/b&gt;Мы смотрим на действия других, чтобы определить собственное поведение.&lt;/p&gt;

	&lt;ul&gt;
		&lt;li aria-level="2" dir="ltr"&gt;
		&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;В дизайне: &lt;/b&gt;Отзывы, количество скачиваний, фразы «Купили 100 человек за последний час», «Ваши друзья используют эту функцию». Это снижает неопределенность и риски.&lt;/p&gt;
		&lt;/li&gt;
	&lt;/ul&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;Дефицит и срочность (Scarcity &amp;amp; Urgency): &lt;/b&gt;Ограниченное количество или время повышает воспринимаемую ценность.&lt;/p&gt;

	&lt;ul&gt;
		&lt;li aria-level="2" dir="ltr"&gt;
		&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;В дизайне: &lt;/b&gt;«Осталось 2 билета по этой цене», «Акция закончится через 2 часа 15 минут», «Ваша корзина забудется через 10 минут». Таймеры создают ощущение FOMO (страх упустить выгоду).&lt;/p&gt;
		&lt;/li&gt;
	&lt;/ul&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;Якорение (Anchoring): &lt;/b&gt;Первая полученная информация (якорь) сильно влияет на последующие оценки.&lt;/p&gt;

	&lt;ul&gt;
		&lt;li aria-level="2" dir="ltr"&gt;
		&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;В дизайне: &lt;/b&gt;Ценовая стратегия. Показав сначала высокую цену «$999», цена «$599» кажется выгодной. В формах подписки часто самый выгодный тариф (якорь) находится в центре.&lt;/p&gt;
		&lt;/li&gt;
	&lt;/ul&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;Эффект простого воздействия (Mere Exposure Effect):&lt;/b&gt; Чем чаще мы сталкиваемся с чем-либо, тем больше это нам нравится.&lt;/p&gt;

	&lt;ul&gt;
		&lt;li aria-level="2" dir="ltr"&gt;
		&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-7cc924ed-7fff-c938-06a3-35bfbb6e4daa"&gt;В дизайне: &lt;/b&gt;Последовательный, повторяющийся брендинг (цвета, шрифты, тональность) на всех точках касания с пользователем повышает узнаваемость и лояльность.&lt;/p&gt;
		&lt;/li&gt;
	&lt;/ul&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;&lt;b id="docs-internal-guid-73ebf76e-7fff-ebfb-d28f-3b3674f057c1"&gt;Синтез — Как это работает вместе в реальном интерфейсе?&lt;/b&gt;&lt;/h2&gt;

&lt;p&gt;Рассмотрим на примере страницы оформления заказа в интернет-магазине:&lt;/p&gt;

&lt;ol&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-0101bbc3-7fff-2b5c-32cb-635885a8fd05"&gt;Когнитивная психология: &lt;/b&gt;Законы близости и общей зоны группируют поля доставки и оплаты в четкие блоки. Контрастный цвет для кнопки «Оформить заказ» выделяет её на фоне остальных элементов (схожесть). Стандартный процесс «Корзина &amp;gt; Оформление &amp;gt; Подтверждение» соответствует ментальной модели покупки.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-0101bbc3-7fff-2b5c-32cb-635885a8fd05"&gt;Поведенческая экономика: &lt;/b&gt;На этапе корзины работает социальное доказательство («Люди, купившие это, также берут...»). На странице оплаты может быть применен дефицит («Бесплатная доставка действует еще 15 мин!»). Тариф «Премиум» служит якорем, делая тариф «Стандарт» более привлекательным. А опция «Сохранить данные карты для будущих покупок» уже включена по умолчанию, используя нашу любовь к статус-кво.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-9c3ebf7c-7fff-4293-4be7-2bf575be924c"&gt;Этика: Великая сила — великая ответственность&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Знание психологии — это обоюдоострый меч. Те же принципы, что помогают пользователю сделать здоровый выбор (подписаться на пенсионные отчисления по умолчанию — nudge), могут быть использованы для создания «темных паттернов».&lt;/p&gt;

&lt;ul&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-16df969a-7fff-aeed-4370-ef6b69bb66f0"&gt;Принуждение: &lt;/b&gt;Сложно найти кнопку «Отписаться от рассылки».&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-16df969a-7fff-aeed-4370-ef6b69bb66f0"&gt;Трюк с подтверждением: &lt;/b&gt;Дорогой товар «случайно» добавляется в корзину.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-16df969a-7fff-aeed-4370-ef6b69bb66f0"&gt;Скрытые затраты: &lt;/b&gt;Окончательная цена показывается только в конце долгого процесса.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;ul&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-16df969a-7fff-aeed-4370-ef6b69bb66f0"&gt;Принуждение: &lt;/b&gt;Сложно найти кнопку «Отписаться от рассылки».&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-16df969a-7fff-aeed-4370-ef6b69bb66f0"&gt;Трюк с подтверждением: &lt;/b&gt;Дорогой товар «случайно» добавляется в корзину.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;&lt;b id="docs-internal-guid-16df969a-7fff-aeed-4370-ef6b69bb66f0"&gt;Скрытые затраты: &lt;/b&gt;Окончательная цена показывается только в конце долгого процесса.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p dir="ltr"&gt;Этичный дизайнер использует психологию для:&lt;/p&gt;

&lt;ul&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Упрощения, а не усложнения.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Просвещения, а не манипуляции.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Расширения возможностей, а не ограничения выбора.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Создания долгосрочного доверия, а не сиюминутной выгоды.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b id="docs-internal-guid-8acbd9a1-7fff-14cc-13eb-f837790ec63f"&gt;Заключение&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Психология в дизайне — это не манипуляция, а гуманизация технологий. Это язык, на котором интерфейс говорит с нашей древней нервной системой и нашим иррациональным, эмоциональным «Я». Понимая законы восприятия гештальта, эмоциональный язык цвета, силу ожиданий и когнитивные искажения, дизайнер перестает быть просто исполнителем. Он становится проводником, который с уважением и точностью ведет пользователя через цифровой ландшафт, помогая ему достигать целей быстро, приятно и — что самое важное — осознанно.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;
Итоговый совет: Внедряйте эти принципы последовательно. Начните с гештальта для ясности, добавьте цвет для эмоций, проверьте ментальные модели через тесты, а затем осторожно применяйте «подталкивания» для достижения бизнес- и пользовательских целей. Всегда спрашивайте себя: «Кому это решение приносит пользу в долгосрочной перспективе?» Ответ должен быть: «В первую очередь, пользователю».&lt;br /&gt;
&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Алексей Кондратьев</dc:creator>
    <dc:date>2026-02-03T16:05:00Z</dc:date>
  </entry>
  <entry>
    <title>Go вместе изучать Go. Часть 5</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21212001" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21212001</id>
    <updated>2026-02-01T13:25:41Z</updated>
    <published>2026-02-01T12:39:00Z</published>
    <summary type="html">&lt;style type="text/css"&gt;article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}

article img {
width: 90%;
}

article li {
 font-size:11pt;   
}

.centered {
text-align:center;
}

article .portlet-msg-info {
color: #232323;
background-color: #f9f9f9;
border-style: dashed;
border-color: #232323;
}
&lt;/style&gt;
&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Введение&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Интерфейсы — один из элегантных инструментов Go, который позволяет писать гибкий и масштабируемый код. В отличие от многих других языков программирования, Go использует имплицитную (неявную) реализацию интерфейсов, что делает их особенно удобными и гибкими.&lt;/p&gt;

&lt;p&gt;Если вы когда-нибудь сталкивались с жёсткой типизацией и сложными иерархиями наследования в других языках, интерфейсы Go принесут в вашу жизнь глоток свежего воздуха. В этой статье мы разберёмся с основами интерфейсов, узнаем, как они работают под капотом, и изучим практические примеры, которые помогут вам использовать их в своих проектах.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Что такое интерфейсы?&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Интерфейс в Go — это набор сигнатур методов. Проще говоря, интерфейс определяет, какие методы должен иметь тип, но не говорит, как эти методы реализовать. Значение типа интерфейса может содержать любое значение, которое реализует эти методы.&lt;/p&gt;

&lt;p&gt;Для начала давайте рассмотрим простой пример:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
type I interface {
	M()
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Здесь мы определили интерфейс I с одним методом M(). Любой тип, который имеет метод M() с такой же сигнатурой, будет реализовывать этот интерфейс — причём совершенно автоматически, без каких-либо явных деклараций.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Неявная реализация интерфейсов&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Одна из самых уникальных черт Go — интерфейсы реализуются неявно. Нет никакого ключевого слова implements или других явных объявлений. Если тип имеет все методы, определённые в интерфейсе, он автоматически реализует этот интерфейс.&lt;/p&gt;

&lt;p&gt;Давайте посмотрим на конкретный пример:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

// Метод M() означает, что тип T реализует интерфейс I
// Но мы не должны явно заявлять об этом
func (t T) M() {
	fmt.Println(t.S)
}

func main() {
	var i I = T{"hello"}
	i.M()
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

hello&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Что здесь происходит?&lt;/strong&gt; Мы объявили переменную i&amp;nbsp;с типом интерфейса&amp;nbsp;I, а затем присвоили ей значение&amp;nbsp;T{"hello"}. Это работает потому, что структура&amp;nbsp;T&amp;nbsp;имеет метод&amp;nbsp;M(), который отвечает требованиям интерфейса&amp;nbsp;I. Go автоматически определил, что тип&amp;nbsp;T&amp;nbsp;реализует интерфейс&amp;nbsp;I, и позволил выполнить это присваивание.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Важный момент&lt;/strong&gt;:&amp;nbsp;имплицитная реализация отделяет определение интерфейса от его реализации. Вы можете определить интерфейс в одном пакете, а реализовать его в совершенно другом пакете — независимо друг от друга, без каких-либо предварительных соглашений. Это мощный инструмент для построения модульных и гибких архитектур.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Значения интерфейсов&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Чтобы по-настоящему понять, как работают интерфейсы в Go, нужно понять, что происходит под капотом. Значение интерфейса можно представить себе как кортеж из двух элементов:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(значение, конкретный_тип)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Интерфейс хранит значение конкретного типа. Когда вы вызываете метод на значении интерфейса, Go вызывает соответствующий метод этого конкретного типа.&lt;/p&gt;

&lt;p&gt;Рассмотрим практический пример:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"math"
)

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	fmt.Println(t.S)
}

type F float64

func (f F) M() {
	fmt.Println(f)
}

func main() {
	var i I

	i = &amp;amp;T{"Hello"}
	describe(i)
	i.M()

	i = F(math.Pi)
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

(&amp;amp;{Hello}, *main.T)

Hello

(3.141592653589793, main.F)

3.141592653589793
&lt;/pre&gt;

&lt;p&gt;Функция describe() показывает нам внутреннее состояние интерфейса: значение и его конкретный тип. Обратите внимание, что мы можем присвоить одной и той же переменной интерфейса значения разных типов. В первом случае это указатель на T, во втором — значение типа F. Go гибко обрабатывает оба случая.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Значения интерфейсов с nil-значениями&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Интересный аспект Go: интерфейс может содержать nil-значение конкретного типа, но сам интерфейс при этом остаётся не nil. Методы в этом случае будут вызваны с nil-приёмником.&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("&amp;lt;nil&amp;gt;")
		return
	}
	fmt.Println(t.S)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()

	i = &amp;amp;T{"hello"}
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

(&amp;lt;nil&amp;gt;, *main.T)

&amp;lt;nil&amp;gt;

(&amp;amp;{hello}, *main.T)

hello&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;В Go это нормальная практика — методы часто пишут так, чтобы они корректно работали с nil-приёмниками. Это предохраняет от неожиданных паник.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Nil-интерфейсы&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Совсем другое дело — интерфейс, который не содержит ни значения, ни конкретного типа. Это настоящий nil-интерфейс:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

type I interface {
	M()
}

func main() {
	var i I
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

(&amp;lt;nil&amp;gt;, &amp;lt;nil&amp;gt;)

panic: runtime error: invalid memory address or nil pointer dereference&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;Когда вы вызываете метод на nil-интерфейсе, Go не знает, какой конкретный метод вызвать, потому что нет информации о типе. Результат — ошибка выполнения (panic). Всегда проверяйте интерфейсы на nil перед использованием!&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Пустой интерфейс&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Интерфейс, который не определяет ни одного метода, называется пустым интерфейсом:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
interface{}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Пустой интерфейс может содержать значение любого типа, потому что каждый тип реализует как минимум ноль методов. Это невероятно полезно, когда вы работаете со значениями неизвестного типа.&lt;/p&gt;

&lt;p&gt;Классический пример — функция fmt.Print(), которая может принимать любое количество аргументов любых типов:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

func main() {
	var i interface{}
	describe(i)

	i = 42
	describe(i)

	i = "hello"
	describe(i)
}

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

(&amp;lt;nil&amp;gt;, &amp;lt;nil&amp;gt;)

(42, int)

(hello, string)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Пустой интерфейс часто используется в стандартной библиотеке Go и в пользовательском коде для создания гибких функций, которые могут работать с любыми типами данных.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Утверждения типов (Type Assertions)&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Когда вы работаете со значением интерфейса и вам нужно получить доступ к конкретному значению, скрытому внутри интерфейса, вы используете утверждение типа.&lt;/p&gt;

&lt;p&gt;Простая форма утверждения типа:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
t := i.(T)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Это утверждает, что интерфейсное значение i содержит конкретный тип T, и присваивает базовое значение переменной t. Если i на самом деле не содержит T, программа перейдёт в состояние паники.&lt;/p&gt;

&lt;p&gt;Более безопасный вариант — использовать двойное возвращаемое значение:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
t, ok := i.(T)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Если i содержит T, то t получит базовое значение и ok будет true. Если нет, ok будет false, t получит нулевое значение типа T, и паника не произойдёт. Это похоже на чтение из map в Go.&lt;/p&gt;

&lt;p&gt;Давайте посмотрим практический пример:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

func main() {
	var i interface{} = "hello"

	// Опасный вариант
	s := i.(string)
	fmt.Println(s)

	// Безопасный вариант с проверкой
	s, ok := i.(string)
	fmt.Println(s, ok)

	// Проверяем тип, который не совпадает
	f, ok := i.(float64)
	fmt.Println(f, ok)

	// Это вызовет панику!
	f = i.(float64)
	fmt.Println(f)
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

hello

hello true

0 false

panic: interface conversion: interface {} is string, not float64&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Совет:&lt;/strong&gt; Всегда используйте безопасный вариант с двойным возвращаемым значением (t, ok := i.(T)), если вы не уверены в типе.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Переключение по типам (Type Switches)&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Когда вам нужно проверить несколько типов одновременно, переключение по типам — это идеальный инструмент. Синтаксис выглядит почти как обычный switch, но вместо значений мы сравниваем типы:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
switch v := i.(type) {
case T:
	// здесь v имеет тип T
case S:
	// здесь v имеет тип S
default:
	// нет совпадений; здесь v имеет тот же тип, что и i
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Рассмотрим практический пример:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

Twice 21 is 42

"hello" is 5 bytes long

I don't know about type bool!&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Type switch автоматически преобразует значение в корректный тип в каждом case. Это намного удобнее, чем писать цепочку утверждений типов.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Интерфейс Stringer&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Одна из самых распространённых интерфейсов в стандартной библиотеке — это Stringer, определённый в пакете fmt:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
type Stringer interface {
	String() string
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Любой тип, реализующий метод String(), может быть красиво отформатирован при печати. Пакет fmt и многие другие ищут этот интерфейс.&lt;/p&gt;

&lt;p&gt;Давайте создадим полезный пример:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
	a := Person{"Arthur Dent", 42}
	z := Person{"Zaphod Beeblebrox", 9001}
	fmt.Println(a, z)
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Без реализации String() вывод был бы просто {Arthur Dent 42} и {Zaphod Beeblebrox 9001}. Метод String() позволяет полностью контролировать, как выглядит ваш тип при печати.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Обработка ошибок с интерфейсами&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;В Go ошибки представлены встроенным интерфейсом:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
type error interface {
	Error() string
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Функции часто возвращают значение типа error. Нулевое значение error означает успех; ненулевое значение означает ошибку:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
i, err := strconv.Atoi("42")
if err != nil {
	fmt.Printf("couldn't convert number: %v\n", err)
	return
}
fmt.Println("Converted integer:", i)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Давайте создадим собственный тип ошибки:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"time"
)

type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s", e.When, e.What)
}

func run() error {
	return &amp;amp;MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Реализуя интерфейс error, вы можете создавать собственные, информативные сообщения об ошибках.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Интерфейс Reader&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Пакет io определяет интерфейс io.Reader, который представляет один конец потока данных:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
func (T) Read(b []byte) (n int, err error)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Метод Read заполняет заданный байтовый срез данными и возвращает количество заполненных байтов и значение ошибки. Он возвращает io.EOF когда поток заканчивается.&lt;/p&gt;

&lt;p&gt;Стандартная библиотека Go содержит много реализаций Reader: файлы, сетевые соединения, компрессоры, шифры и многое другое. Вот практический пример:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

n = 8 err = &amp;lt;nil&amp;gt; b = [72 101 108 108 111 44 32 82]

b[:n] = "Hello, R"

n = 6 err = &amp;lt;nil&amp;gt; b = [101 97 100 101 114 33 32 82]

b[:n] = "eader!"

n = 0 err = EOF b = [101 97 100 101 114 33 32 82]

b[:n] = ""&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Интерфейс Reader — один из мощных инструментов Go, позволяющий писать код, который работает с любыми потоками данных единообразно.&lt;/p&gt;

&lt;h2&gt;Бонус: Различие между значениями и указателями&lt;/h2&gt;

&lt;p&gt;Очень важно понимать разницу между методами на значениях и методами на указателях. Давайте рассмотрим пример, где эта разница критична:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"math"
)

type Abser interface {
	Abs() float64
}

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	a = f       // MyFloat реализует Abser (метод на значении)
	a = &amp;amp;v      // *Vertex реализует Abser (метод на указателе)
	
	// ОШИБКА: v имеет тип Vertex (не *Vertex) и НЕ реализует Abser
	// Метод Abs определён только на *Vertex
	a = v       // Это вызовет ошибку компиляции!

	fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f &amp;lt; 0 {
		return float64(-f)
	}
	return float64(f)
}

type Vertex struct {
	X, Y float64
}

// Заметьте: приёмник (receiver) — это указатель *Vertex
func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="portlet-msg-info"&gt;
&lt;strong&gt;Результат: &lt;/strong&gt;

cannot use v (variable of struct type Vertex) as Abser value in assignment:

Vertex does not implement Abser (method Abs has pointer receiver)&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Важное правило: &lt;/strong&gt;Если метод определён на указателе *T, то интерфейс можно реализовать только через указатель. Если метод определён на значении T, то интерфейс можно реализовать как через значение, так и через указатель (потому что Go автоматически дереферирует указатели при необходимости).&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Заключение&lt;/h2&gt;

&lt;p&gt;Интерфейсы в Go — это мощный инструмент для написания гибкого и модульного кода.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;В этой статье мы научились:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Базовым понятиям интерфейсов: как определять и использовать набор методов без явных объявлений реализации&lt;/li&gt;
	&lt;li&gt;Неявной реализации интерфейсов в Go, которая автоматически определяет, реализует ли тип данные методы&lt;/li&gt;
	&lt;li&gt;Структуре интерфейсов: как они хранят значение и его конкретный тип внутри своей внутренней реализации&lt;/li&gt;
	&lt;li&gt;Доступу к базовым типам через утверждения типов и как безопасно работать с ними&lt;/li&gt;
	&lt;li&gt;Использованию пустого интерфейса для работы со значениями неизвестного типа&lt;/li&gt;
	&lt;li&gt;Практическому применению интерфейсов в стандартной библиотеке Go и как они помогают писать чистый, гибкий код&lt;/li&gt;
	&lt;li&gt;Критической разнице между методами на значениях и указателях, как она влияет на реализацию интерфейсов&lt;/li&gt;
	&lt;li&gt;Методам работы с ошибками, потоками данных, и настройке форматирования выводов через стандартные интерфейсы&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Практикуя применение интерфейсов, вы сможете писать более гибкий, расширяемый и поддерживаемый код в Go.&lt;/p&gt;</summary>
    <dc:creator>Romo Fedoroff</dc:creator>
    <dc:date>2026-02-01T12:39:00Z</dc:date>
  </entry>
  <entry>
    <title>Swagger: как починить неработающие разнородные multipart-запросы</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21211280" />
    <author>
      <name>Никита Рогаленко</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21211280</id>
    <updated>2026-01-30T16:45:06Z</updated>
    <published>2026-01-30T16:30:00Z</published>
    <summary type="html">&lt;p style="text-align: justify;"&gt;Swagger давно уже можно назвать своего рода стандартом среди инструментов для документирования и тестирования RESTful API. Вместо ручного написания curl’ов для проверки созданных эндпоинтов всегда удобнее воспользоваться веб-интерфейсом Swagger UI. Тем не менее в каждом решении бывают свои недоработки, баги и проблемы. Сегодня рассмотрим одну из проблем, с которой мы столкнулись при использовании Swagger в процессе разработки сервиса на базе фреймворка Spring. Проблема некритичная, но порой раздражающая и тормозящая рабочий процесс, и связана она с особенностью обработки сложных multipart запросов с разнородными компонентами в теле. Перед тем как продолжить, сразу обозначим, что нами используется актуальная спецификация OpenAPI 3.0 (в более ранних версиях Swagger с поддержкой multipart-запросов все, кажется, совсем плохо).&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Итак, рассмотрим классический пример. Мы пишем Spring-приложение на Kotlin, написан простой контроллер для обработки входящих запросов, в котором есть эндпоинт, принимающий POST-запросы, содержащие в себе какой-либо файл и дополнительную метаинформацию о файле в формате JSON. Заголовок &lt;strong&gt;Content-Type&lt;/strong&gt; у такого запроса будет &lt;strong&gt;multipart/form-data;&lt;/strong&gt;, при этом запрос у нас сложный, гетерогенный, в котором разные части тела запроса представлены разными типами контента. Упрощенно контроллер будет выглядеть примерно так:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

@RestController
@RequestMapping("/files")
class FilesController(
    private val fileService: FileService,
) {

    @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
    fun processFile(
        @RequestPart("info") metaInfo: FileInfoDto,
        @RequestPart("file") file: MultipartFile,
    ): ResponseEntity&amp;lt;ResultDto&amp;gt; {
        val executionResult = fileService.processNewFile(metaInfo, file.inputStream)
        return ResponseEntity.status(HttpStatus.CREATED).body(executionResult)
    }

}&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;Содержимое конкретных DTO и сервисов нас сейчас не интересует, просто представим, что все написано без ошибок и соответствует требованиям к системе. С точки зрения программного кода ошибок нет, сервис функционирует и успешно обрабатывает запросы. Но только если они посылаются не из Swagger UI. В веб-интерфейсе отправим примерно такой запрос, добавив произвольный файл&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;img height="369" src="https://www.tune-it.ru/documents/portlet_file_entry/20567281/swagger.png/6b8532ec-6e7c-07d7-4369-f81479979ee4?imagePreview=1" width="1110" /&gt;&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;В ответ мы с большой долей вероятности получим нечто вроде:&lt;/p&gt;

&lt;pre class="brush:plain;"&gt;
org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/octet-stream' is not supported&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;Это означает, что Spring "не понял", что за тип данных нам отправляется, посчитав пришедшее на сервер недифференцируемым потоком бинарных данных. Spring не может сопоставить части запроса с описанными нами RequestPart, понять, где мы послали JSON, а где файл, из-за чего все разваливается. Но проблема тут не на стороне приложения, а именно в отсутствии нужной конфигурации для Swagger. Мы это поймем, если в веб-интерфейсе посмотрим curl, который swagger сгенерировал для данного запроса:&lt;/p&gt;

&lt;pre class="brush:bash;"&gt;
curl -X 'POST' \
  'http://localhost:8080/o/api/files' \
  -H 'accept: */*' \
  -H 'Content-Type: multipart/form-data' \
  -F 'info={"authorId": 123, "description": "something was created"}' \
  -F 'file=@img.png;type=image/png'&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;На первый взгляд может показаться, что все в порядке, но проблема в том, что в строке с info после JSON'а нет куска &lt;strong&gt;;type=application/json&lt;/strong&gt;&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;На уровне своей архитектуры OpenAPI 3.0 умеет работать с такими запросами, но нужно немного потрудиться над конфигурацией, чтобы Swagger понял, какая часть запроса каким типом представлена. Файлы Swagger способен распознать и добавить им тип по умолчанию, с JSON строкой же такого не происходит, в схеме нет нужной инструкции, а по умолчанию все строки в multipart-запросах Swagger считает типом &lt;strong&gt;text/plain&lt;/strong&gt;. Таким образом, если Spring понимает, что в RequestPart ожидается JSON и способен его распарсить, то Swagger по умолчанию нет, он до последнего будет считать JSON произвольной строкой и отказываться добавлять в curl нужный тип.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;К счастью, есть может и громоздкий, но рабочий способ исправления этой проблемы, который позволит жестко привязать тип контента к заданным частям тела запроса. Делается это с помощью специальных аннотаций swagger. Перепишем представленный ранее контроллер, чтобы все заработало как нужно:&lt;/p&gt;

&lt;pre class="brush:java;"&gt;
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Encoding​​​​​​​

@RestController
​​​​​​​@Tag(name="Контроллер с правильной конфигурацией swagger")
@RequestMapping("/files")
class FilesController(
    private val fileService: FileService,
) {

​​​​​​​   @Operation(
        summary = "Демонстрационный эндпоинт",
        requestBody = io.swagger.v3.oas.annotations.parameters.RequestBody(
            content = [
                Content(
                    mediaType = MediaType.MULTIPART_FORM_DATA_VALUE,
                    encoding = [
                        Encoding(name = "info", contentType = "application/json")
                    ]
                )
            ]
        )
    )
    @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
    fun processFile(
        @RequestPart("info") metaInfo: FileInfoDto,
        @RequestPart("file") file: MultipartFile,
    ): ResponseEntity&amp;lt;ResultDto&amp;gt; {
        val executionResult = fileService.processNewFile(metaInfo, file.inputStream)
        return ResponseEntity.status(HttpStatus.CREATED).body(executionResult)
    }

}&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;После перезапуска приложения все должно заработать, запрос, отправленный из Swagger, обработается успешно, а curl станет генерироваться корректно:&lt;/p&gt;

&lt;pre class="brush:bash;"&gt;
curl -X 'POST' \
  'http://localhost:8080/o/api/files' \
  -H 'accept: */*' \
  -H 'Content-Type: multipart/form-data' \
  -F 'info={
  "authorId": 123,
  "description": "something was created"
};type=application/json' \
  -F 'file=@img.png;type=image/png'&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Никита Рогаленко</dc:creator>
    <dc:date>2026-01-30T16:30:00Z</dc:date>
  </entry>
  <entry>
    <title>Стили менеджмента: классификация по PAEI</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21207336" />
    <author>
      <name>Vadim Mikhu</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21207336</id>
    <updated>2026-01-21T10:14:43Z</updated>
    <published>2026-01-20T15:17:00Z</published>
    <summary type="html">&lt;h2&gt;Введение&lt;/h2&gt;

&lt;p&gt;В своей практике на работе, если вы захотите проявить инициативу, или просто будете достаточно ответственны, то обязательно встретитесь с необходимостью руководить другими людьми. Cегодня мы поговорим не об управляемых, а об управляющих. Какие бывают стили менеджмента, и как понять, кем вы сами являетесь как руководитель? Для этого используем классификацию PAEI.&lt;br /&gt;
&lt;br /&gt;
&lt;em&gt;Примечание: Эта статья является выдержкой из глав книги "Идеальный руководитель" Ицхака Адизеса. Большинство идеи и фраз&amp;nbsp;заимствованы из книги или являются прямыми цитатами.&amp;nbsp;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;Что такое "менеджмент"? Задачи менеджмента&lt;/h2&gt;

&lt;p&gt;&lt;cite&gt;Кто мудр? Тот, кто учится у всех и каждого.&lt;br /&gt;
Кто силен? Тот, кто обуздал свои страсти.&lt;br /&gt;
Кто богат? Тот, кто доволен судьбой.&lt;br /&gt;
Кому это дано?&lt;br /&gt;
Никому.&lt;br /&gt;
Бенджамин Франклин&lt;/cite&gt;&lt;br /&gt;
&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Согласно классическим учебникам и популярным руководствам по менеджменту идеальный менеджер должен быть знающим, целеустремленным, дотошным, методичным и расторопным. Он организован, рационален, и рассудителен. Он - наделенный харизмой провидец, который готов идти на риск и приветствует преобразования. Он отзывчив и чуток к потребностям людей.&lt;/p&gt;

&lt;p&gt;Идеальный менеджер умеет объединить всех необходимых специалистов, мобилизовав их на достижение поставленных целей. Он создает команду, способную выполнять свои функции самостоятельно, без его контроля. Он оценивает собственную деятельность по результатам работы своей команды, определяя, насколько успешно его подчиненные вместе и по отдельности решают поставленные перед ними задачи и насколько эффективно помогает им в этом он сам.&lt;br /&gt;
&lt;br /&gt;
Несколько пунктов, которые характеризуют менеджмент как явление:&lt;/p&gt;

&lt;ol&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Предполагает иерархию.&lt;/strong&gt;&lt;/em&gt; Менеджерами обычно называют группу управленцев - уровнем выше, чем руководители низшего звена, и уровнем ниже, чем высшее руководство.&lt;/li&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Носит однонаправленный характер.&lt;/strong&gt;&lt;/em&gt; Мотивировать в контексте менеджмента означает: лицо, которое создает мотивацию, заранее знает, что нужно сделать; суть мотивации в том, чтобы заставить другого сделать это добровольно.&lt;/li&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Менеджмент - удел избранных.&lt;/strong&gt;&lt;/em&gt; Это не только наука и искусство, но и выражение социально-политических ценностей.&lt;/li&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Носит индивидуалистический характер.&lt;/strong&gt;&lt;/em&gt; Считается, что один-единственный менеджер должен олицетворять собой весь процесс управления, обладая непревзойденными навыками планирования, организации, создания мотивации, коммуникации и создания эффективных команд. Однако в реальности такого менеджера попросту не существует. Под менеджментом имеется в виду&amp;nbsp;не человек, а процесс.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;&lt;em&gt;Ориентирован прежде всего на промышленность.&amp;nbsp;&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;&lt;em&gt;Носит отпечаток социально-полтического устройства.&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Обусловлен культурными факторами.&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Код (PAEI)&lt;/h2&gt;

&lt;p&gt;Конечная цель процесса управления - сделать организацию результативной и эффективной в ближайшем и долгосрочном перспективе, этого достаточно для благополучия и успеха любой организационной структуры. Как организация оценивает свой успех - вопрос вторичный.&lt;/p&gt;

&lt;p&gt;"Около 40 лет назад я обнаружил, что для обеспечения результативности и эффективности организации ... необходимо выполнять четыре функции. Каждая из них необходима, а вместе они &lt;em&gt;достаточны&lt;/em&gt; для хорошего управления. Слово 'необходимо' подразумевает, что если хотя бы одна из функций не выполняется, имеет место определенная модель неправильного менеджмента."&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Первая функция, которую должен выполнять менеджмент в любой организации - это (P), или &lt;em&gt;&lt;strong&gt;производство результатов&lt;/strong&gt;&lt;/em&gt;, обеспечивающее результативность организации в краткосрочном аспекте. Почему люди обращаются к вашей компании? Для чего вы им нужны? Какие услуги им требуются?&lt;/li&gt;
	&lt;li&gt;Вторая функция, (А), или &lt;em&gt;&lt;strong&gt;администрирование&lt;/strong&gt;&lt;/em&gt;, нужна, чтобы следить за порядком в организационных процессах: компания должна делать правильные вещи в правильной последовательности с правильной интенсивностью.Задача администратора (А) — обеспечить эффективность в краткосрочном аспекте.&lt;/li&gt;
	&lt;li&gt;Далее нам понадобится провидец. Он определяет направление, которого должна придерживаться организация. Это &lt;em&gt;&lt;strong&gt;функция предпринимателя&lt;/strong&gt;&lt;/em&gt; (Е), который сочетает в себе творческий подход и готовность идти на риск. Если организация успешно справляется с выполнением этой функции, ее услуги и/или продукты будут пользоваться спросом у будущих клиентов.&lt;/li&gt;
	&lt;li&gt;И наконец, менеджмент должен обеспечить &lt;em&gt;&lt;strong&gt;интеграцию&lt;/strong&gt;&lt;/em&gt; (I), то есть создать такую атмосферу и систему ценностей, которые будут стимулировать людей действовать сообща и не дадут никому стать незаменимым, что обеспечит жизнеспособность и эффективность организации в долгосрочной перспективе.&lt;/li&gt;
&lt;/ul&gt;

&lt;table border="1" cellpadding="5"&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;ВХОД&lt;/td&gt;
			&lt;td&gt;ПРЕОБРАЗОВАНИЕ&lt;/td&gt;
			&lt;td colspan="2"&gt;ВЫХОД&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;Функции&lt;/td&gt;
			&lt;td&gt;Для превращения организации в …&lt;/td&gt;
			&lt;td&gt;Характеризующуюся&lt;/td&gt;
			&lt;td&gt;На временном горизонте&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;(P) Производить результаты&lt;/td&gt;
			&lt;td&gt;Функциональную&lt;/td&gt;
			&lt;td&gt;Результативностью&lt;/td&gt;
			&lt;td&gt;В краткосрочном аспекте&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;(A) Администрировать&lt;/td&gt;
			&lt;td&gt;Систематизированную&lt;/td&gt;
			&lt;td&gt;Эффективностью&lt;/td&gt;
			&lt;td&gt;В краткосрочном аспекте&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;(E) Быть предпринимателем&lt;/td&gt;
			&lt;td&gt;Готовую к упреждающим действиям&lt;/td&gt;
			&lt;td&gt;Результативностью&lt;/td&gt;
			&lt;td&gt;В долгосрочной перспективе&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;(I)Интегрировать&lt;/td&gt;
			&lt;td&gt;Единый организм&lt;/td&gt;
			&lt;td&gt;Эффективностью&lt;/td&gt;
			&lt;td&gt;В долгосрочной перспективе&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;br /&gt;
(P): Что&amp;nbsp;нужно сделать?&lt;br /&gt;
(A): Как это нужно сделать?&lt;br /&gt;
(E): Когда/зачем это нужно сделать?&lt;br /&gt;
(I): Кто это должен сделать?&lt;/p&gt;

&lt;h2&gt;Стили менеджмента&lt;/h2&gt;

&lt;p&gt;С помощью четырех названных функций можно кратко описать множество явлений. Применительно к стилям управления мы получим сокращенные обозначения,&amp;nbsp;которые позволяют определить "стиль" через комбинацию успешно осуществляемых функций. Если эта комбинация известна, стиль предсказуем.&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Производитель (Paei)&lt;/strong&gt;&lt;/em&gt; - &lt;em&gt;"Давайте посмотрим, что представляет собой стиль менеджера, который успешно выполняет (Р)-Функцию, обеспечивая создание продукта, необ-ходимого для удовлетворения потребностей клиентов, то есть производство желаемого результата, и удовлетворительно справляется с администриро-ванием, предпринимательством и интеграцией. Такого менеджера, обо-значенного кодом (Раеі), я называю производителем, или менеджером (Р)-типа."&lt;/em&gt;&lt;br /&gt;
	&amp;nbsp;&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;&lt;em&gt;Администратор (pAei)&lt;/em&gt;&lt;/strong&gt; -&amp;nbsp;Такой человек имеет природную склонность замечать детали, в особенности касающиеся внедрения. Он методичен и любит, чтобы рабочая среда была продумана ихорошо организована него линейный способ&amp;nbsp;машения. Когда у вас возникает идея, связанная с бизнесом, особенно если это безумная идея или если вы опасаетесь, что она окажется безумной, - вы отправляетесь к подобному менеджеру, чтобы он охладил ваш энтузиазм. Он сумеет оценить суть дела. Он задаст вопросы, которые не приходили вам в голову. Он увидит все подводные камни, которые вы не учли. Дайте ему прочесть бизнес-план, и он порвет его в клочья. И вы будете ему благодарны! Предвидя проблемы, можно решить их, прежде чем они переросли в кризис, или отказаться от несостоятельного плана и таким образом снизить затраты и убытки в долгосрочной перспективе.&lt;br /&gt;
	&amp;nbsp;&lt;/li&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Генератор идей (paEi)&lt;/strong&gt;&lt;/em&gt; -&amp;nbsp;Менеджер такого типа — не совсем предприниматель. Чтобы стать предпринимателем, который создает организации и обеспечивает их развитие, нужно одновременно иметь развитые (Р)-навыки. Ориентации только на (E) недостаточно.&lt;br /&gt;
	Того, кто по большей части нацелен на (E) и удовлетворительно, но не блестяще справляется с (Р)-функцией, я теперь называю Генератором идей. У этого менеджера масса предложений — одни удачные, другие не слишком. Он выдает их в изобилии, иногда это настоящий поток идей. Он подобен школьнику, который тянет руку, не дослушав вопрос учителя. Именно он больше всех говорит на собраниях. Какое бы решение ни было предложено, у него есть другой вариант.&lt;br /&gt;
	&amp;nbsp;&lt;/li&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Предприниматель (paEi)&lt;/strong&gt;&lt;/em&gt; - Чтобы быть предпринимателем, менеджеру необходимо обладать двумя основными качествами. Прежде всего, он должен быть творческой лично-стью, способной намечать новые направления и изобретать стратегии, которые позволяют организации адаптироваться к постоянно меняющимся условиям окружающей обстановки. Чтобы определять стратегию реакции на изменения, он должен чувствовать сильные и слабые стороны своей организации и обладать воображением и смелостью [8].&lt;br /&gt;
	И все же быть творческой личностью недостаточно. Встречаются чрезвычайно творческие люди, которых нельзя назвать предпринимателями.&lt;br /&gt;
	&amp;nbsp;&lt;/li&gt;
	&lt;li&gt;&lt;em&gt;&lt;strong&gt;Интегратор (paeI)&lt;/strong&gt;&lt;/em&gt; -&amp;nbsp;Востоящая интеграция, или интерация, направленная вверх, это способность объединять людей, имеющих более высокий статус, полномочия, должности и т.д. Горизонтальная интеграция — это способность создавать сплоченную группу из равных себе. Нисходящая интеграция, или интеграция, направленная вниз, позволяет стать лидером, сплачивая под-чиненных. Успешный горизонтальный интегратор может с трудом справляться с нисходящей интеграцией, имея склонность слишком надменно держаться с подчиненными. На самом деле редко кто бывает непревзойденным интегратором по всем трем направлениям.&amp;nbsp;Интегратор тонко чувствует других людей, сопереживает им и способен к дедуктивному мышлению — он понимает, чем отличается сказанное от того, что человеку хочется сказать. У него самого есть ряд личностных про-блем, что позволяет ему откликаться на чаяния, проблемы и нужды других людей, ставя их выше собственных интересов.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Подведение итогов&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Прежде чем двигаться дальше, позвольте мне резюмировать изложенные мысли. «Менеджмент» определяется как процесс, который позволяет организации стать и оставаться результативной и эффективной ныне и впредь. Я полагаю что таковы цели любой организации, независимо от технологии, размера, культуры и критериев оценки ее успеха.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Организация достигает этих целей, если успешно выполняются четыре&lt;br /&gt;
Функции: производство во имя удовлетворения ожидаемых потребностей клиентов, администрирование, предпринимательство и интеграция — или (PAEI). Иными словами, организация должна быть нацелена на результат (Р), быть гибкой и хорошо адаптироваться к изменениям (Е), причем такая гибкость должна контролироваться и давать предсказуемые результаты (А). И наконец, система должна быть самонастраивающейся (I) и не требовать корректирующих воздействий извне.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Следовательно, задача менеджмента — выполнять эти четыре функции, поскольку они не реализуются сами по себе. «Управлять» — значит выполнять все эти функции или любую из них, независимо от должности индивида или его места в иерархии — и даже независимо от того, числится ли он в штате. Наверное, теперь, когда мы определили, что такое «менеджмент», и знаем, что ищем, мы сумеем найти идеального менеджера?&lt;br /&gt;
Не тут-то было. Но зато теперь нам будет проще понять, почему идеальных менеджеров не бывает и не может быть."&lt;/em&gt;&lt;/p&gt;</summary>
    <dc:creator>Vadim Mikhu</dc:creator>
    <dc:date>2026-01-20T15:17:00Z</dc:date>
  </entry>
  <entry>
    <title>Задача по динамическому программированию: «Карьерный путь в Tune-IT»</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21203995" />
    <author>
      <name>Polina Napolskaya</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21203995</id>
    <updated>2026-01-14T15:06:26Z</updated>
    <published>2026-01-12T09:38:00Z</published>
    <summary type="html">&lt;h2 dir="ltr" id="docs-internal-guid-c240bf2f-7fff-034c-8503-5c170645bc91"&gt;Задача по динамическому программированию: «Карьерный путь в Tune-IT»&lt;/h2&gt;

&lt;h3 dir="ltr"&gt;Условия задачи&lt;/h3&gt;

&lt;p dir="ltr"&gt;В компании Tune-IT программист строит карьеру, зарабатывая славу, деньги и влияние в зависимости от выбранного пути. Слава, деньги и влияние дают &lt;strong&gt;A, B и C &lt;/strong&gt;очков&lt;strong&gt; репутации&lt;/strong&gt; и &lt;strong&gt;D, E, F &lt;/strong&gt;очков &lt;strong&gt;выгорания &lt;/strong&gt;соответственно. Программист хоть и мечтает получить как можно больше репутации, но если он выгорит, то больше не сможет писать чистый, легко-масштабируемый код, способный менять мир к лучшему. У каждого программиста есть свой предельный уровень &lt;strong&gt;выгорания H&lt;/strong&gt;, после которого он начинает думать об увольнении. Также дана карта возможностей &lt;strong&gt;M x N,&lt;/strong&gt; показывающая, где можно получить славу, деньги и влияние,&amp;nbsp; а где – ничего. На карте возможности отмечены буквами “С”, “Д”, “В” и “Н“.&amp;nbsp;&lt;/p&gt;

&lt;h3 dir="ltr"&gt;Задача&lt;/h3&gt;

&lt;p dir="ltr"&gt;Необходимо найти такой карьерный путь (на карте возможностей), который обеспечит программисту в Tune-IT максимальную репутацию, и при этом не даст ему выгореть. Двигаться по карте возможностей можно на клетку вверх, вниз, вправо и влево начиная с верхней левой клетки. В качестве результата необходимо вывести максимальное число очков репутации. В случае, когда любой выбор карьерного пути приводит к выгоранию, выведите -1.&lt;/p&gt;

&lt;h3 dir="ltr"&gt;Входные данные&lt;/h3&gt;

&lt;ul&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;H – предельный уровень выгорания&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;M, N – размер матрицы возможностей&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;A, B и C – очки репутации, которые дает слава, деньги и влияние соответственно&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;D, E и F – очки выгорания, которые дает слава, деньги и влияние соответственно&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;availability – матрица возможностей M x N, заполненная буквами “С”, “Д”, “В” и “Н“&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 dir="ltr"&gt;Выходные данные&lt;/h3&gt;

&lt;ul&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Максимальное число очков репутации или -1, если любой выбор карьерного пути приводит к выгоранию&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 dir="ltr"&gt;Примеры&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;​​​​​​Ввод:&lt;/em&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;em&gt;1&lt;/em&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;em&gt;1 1&lt;/em&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;em&gt;1 2 3&lt;/em&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;em&gt;1 2 3&lt;/em&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;em&gt;Н&lt;/em&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;em&gt;Вывод:&lt;/em&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;em&gt;0&lt;/em&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;Если без воды и кратко – нужно найти путь по сетке M×N с движениями в 4 направления, который максимизирует суммарную репутацию при ограничении на суммарный уровень выгорания (не превышать H). Каждая клетка даёт либо один из трёх типов вознаграждений (репутация + выгорание) либо ничего. Если все пути приводят к выгоранию (&amp;gt; H), ответ – -1.&lt;/p&gt;

&lt;p dir="ltr"&gt;Это задача оптимизации с ограничением ресурса (burnout) на графе-решётке. Подход динамического программирования на клетках с учётом расходуемого ресурса (выгорания) даёт решение за O(M·N·(H+1)). Однако можно сделать O(M·N) по состояниям, если воспользоваться тем, что значения вознаграждений и издержек фиксированы и не зависят от скорости: здесь разумное и простое решение – BFS/динамика в пространстве (i, j, b), где b – текущее выгорание (0..H). Это динамика на взвешенном графе с неотрицательными прибавками: для каждого состояния храним максимальную репутацию, достижимую при данном выгорании. Переходы увеличивают выгорание на D/E/F (или 0) и репутацию на A/B/C (или 0) при заходе в соседнюю клетку.&lt;/p&gt;

&lt;h3 dir="ltr"&gt;Ключевые шаги решения&lt;/h3&gt;

&lt;ol&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Кодируем для каждой буквы репутацию и выгорание: С→(A,D), Д→(B,E), В→(C,F), Н→(0,0).&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Состояние – (i,j,b) где b ∈ [0..H] – текущая клетка и накопленное выгорание. Храним best[i][j][b] = максимальная репутация при достижении этого состояния.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Инициализация: стартовое состояние – верхняя левая клетка (0,0). Если её burn &amp;gt; H, ответ −1 (нельзя даже стартовать). Иначе стартовое best[0][0][burn0] = reward0. При старте мы посещаем (0,0) и применяем её эффекты.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;По очереди (BFS/deque) релаксируем переходы в 4 направления: для соседа nb = b + burn(neigh); если nb ≤ H, обновляем best[ni][nj][nb] = max(...).&lt;/p&gt;
	&lt;/li&gt;
	&lt;li aria-level="1" dir="ltr"&gt;
	&lt;p dir="ltr" role="presentation"&gt;Ответ – максимум best[i][j][b] по всем i,j,b. Если ни одно состояние недостижимо – выводим - 1.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ol&gt;

&lt;p dir="ltr" role="presentation"&gt;&amp;nbsp;&lt;/p&gt;

&lt;h3 dir="ltr" role="presentation"&gt;Код алгоритма на kotlin:&lt;/h3&gt;

&lt;pre class="brush:java;"&gt;
​​​​​​​import java.io.BufferedReader

import java.io.InputStreamReader

import java.util.ArrayDeque

import kotlin.math.max

data class S(val i:Int, val j:Int, val b:Int)
 

fun main(){

    val br = BufferedReader(InputStreamReader(System.`in`))

    val toks = ArrayDeque&amp;lt;String&amp;gt;()

    fun next(): String {

        while(toks.isEmpty()){

            val line = br.readLine() ?: return ""

            line.trim().split(Regex("\\s+")).filter{it.isNotEmpty()}.forEach{toks.addLast(it)}

        }

        return toks.removeFirst()

    }
 

    val H = next().toInt()

    val M = next().toInt(); val N = next().toInt()

    val A = next().toInt(); val B = next().toInt(); val C = next().toInt()

    val D = next().toInt(); val E = next().toInt(); val F = next().toInt()
 

    val g = Array(M){ CharArray(N) }

    for(i in 0 until M){

        var s = next()

        if(s.length &amp;lt; N){

            val sb = StringBuilder(s)

            while(sb.length &amp;lt; N) sb.append(next())

            s = sb.toString()

        }

        for(j in 0 until N) g[i][j] = s[j]

    }
 

    fun reward(c:Char) = when(c){ 'С','C'-&amp;gt;A; 'Д'-&amp;gt;B; 'В','V'-&amp;gt;C; else-&amp;gt;0 }

    fun burn(c:Char) = when(c){ 'С','C'-&amp;gt;D; 'Д'-&amp;gt;E; 'В','V'-&amp;gt;F; else-&amp;gt;0 }
 

    val NEG = Int.MIN_VALUE/4

    val best = Array(M){ Array(N){ IntArray(H+1){ NEG } } }

    val q = ArrayDeque&amp;lt;S&amp;gt;()
 

    val sb0 = burn(g[0][0])

    if(sb0 &amp;gt; H){ println(-1); return }

    best[0][0][sb0] = reward(g[0][0])

    q.add(S(0,0,sb0))
 

    val di = intArrayOf(-1,1,0,0); val dj = intArrayOf(0,0,-1,1)

    while(q.isNotEmpty()){

        val cur = q.removeFirst()

        val curV = best[cur.i][cur.j][cur.b]

        for(d in 0..3){

            val ni = cur.i + di[d]; val nj = cur.j + dj[d]

            if(ni !in 0 until M || nj !in 0 until N) continue

            val nb = cur.b + burn(g[ni][nj])

            if(nb &amp;gt; H) continue

            val nv = curV + reward(g[ni][nj])

            if(nv &amp;gt; best[ni][nj][nb]){

                best[ni][nj][nb] = nv

                q.add(S(ni,nj,nb))

            }

        }

    }
 

    var ans = NEG

    for(i in 0 until M) for(j in 0 until N) for(b in 0..H) ans = max(ans, best[i][j][b])

    println(if(ans &amp;lt;= NEG/2) -1 else ans)

}

&lt;/pre&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Polina Napolskaya</dc:creator>
    <dc:date>2026-01-12T09:38:00Z</dc:date>
  </entry>
  <entry>
    <title>Go вместе изучать Go. Часть 4</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21202234" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21202234</id>
    <updated>2026-01-07T21:09:43Z</updated>
    <published>2026-01-07T20:40:00Z</published>
    <summary type="html">&lt;style type="text/css"&gt;article p {
font-size:11pt;
font-family:Verdana, sans-serif;
text-align:justify;
color:#6a6a6a;
}

article img {
width: 90%;
}

article li {
 font-size:11pt;   
}

.centered {
text-align:center;
}
&lt;/style&gt;
&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Введение&lt;/h2&gt;

&lt;p&gt;Одной из особенностей в Go, которая часто озадачивает разработчиков из других языков программирования, — является отсутствие классов.&lt;/p&gt;

&lt;p&gt;Однако это не означает, что мы не можем организовать код в объектно-ориентированном стиле. Вместо классов в Go используются методы и структуры.&lt;/p&gt;

&lt;p&gt;В этой статье мы разберём, как работают методы в Go, в чём различия между методами со значением и указателем в качестве получателя, и когда использовать каждый из них.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Что такое метод в Go?&lt;/h2&gt;

&lt;p&gt;Метод в Go — это функция со специальным аргументом-получателем (receiver). Получатель появляется в собственном списке аргументов между ключевым словом func и названием метода.&lt;/p&gt;

&lt;p&gt;Вот простой пример:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

// Вывод: 5&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Здесь (v Vertex) — это получатель метода. Это означает, что метод Abs() принадлежит типу Vertex и может быть вызван на переменной этого типа через точку: v.Abs().&lt;/p&gt;

&lt;p&gt;Почему это важно? Методы позволяют связать функциональность с конкретным типом данных, делая код более организованным и интуитивным. Вместо того чтобы вызывать Abs(v), мы вызываем v.Abs(), что лучше читается и отражает объектно-ориентированный стиль.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Методы — это просто функции с получателем&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Важно понимать, что метод в Go — это не что иное, как обычная функция с дополнительным аргументом. Предыдущий пример можно переписать как функцию:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(Abs(v))
}

// Вывод: 5&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Функциональность идентична, но синтаксис отличается. В первом случае мы используем метод, во втором — функцию. Go предоставляет оба способа для гибкости.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Методы на пользовательских типах&lt;/h2&gt;

&lt;p&gt;Методы можно определять не только на структурах, но и на любых типах, определённых в том же пакете. Например, можно создать метод на базовом числовом типе:&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f &amp;lt; 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}

// Вывод: 1.4142135623730951&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Ограничение: Вы можете определить метод только на типе, определённом в том же пакете. Например, нельзя добавить метод к встроенному типу int в другом пакете. Это ограничение помогает избежать конфликтов и обеспечивает чистоту архитектуры.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Получатели-указатели&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Мы подошли к важному аспекту методов в Go — различию между получателем-значением и получателем-указателем.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Получатель-значение означает, что метод работает с копией исходного значения:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;Получатель-указатель означает, что метод работает с указателем на исходное значение и может его изменять:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Давайте рассмотрим полный пример, чтобы увидеть разницу:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}

// Вывод: 50&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Метод Abs() с получателем-значением просто вычисляет длину вектора. Метод Scale() с получателем-указателем изменяет координаты исходной структуры. Если бы мы использовали значение-получатель для Scale(), изменения затронули бы только копию, а исходная структура осталась бы неизменной.&lt;/p&gt;

&lt;p&gt;Почему это важно? В Go, когда вы передаёте значение функции, она получает копию этого значения. Если структура большая, создание копии может быть неэффективно. Если вам нужно изменить исходное значение, вам необходим указатель.&lt;/p&gt;

&lt;h2&gt;Автоматическое разыменование указателей&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Go предоставляет удобство — методы с получателем-указателем можно вызывать на значениях, и Go автоматически создаст указатель:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
var v Vertex
v.Scale(5)  // Go автоматически интерпретирует это как (&amp;amp;v).Scale(5)&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Аналогично, методы с получателем-значением можно вызывать на указателях:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
var p *Vertex = &amp;amp;Vertex{3, 4}
p.Abs()  // Go автоматически интерпретирует это как (*p).Abs()&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Вот полный пример, демонстрирующий обе ситуации:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(2)        // OK: v — значение, но метод имеет получателя-указатель
	
	p := &amp;amp;Vertex{4, 3}
	p.Scale(3)        // OK: p — указатель на метод с получателем-указателем
	
	fmt.Println(v.Abs())     // OK: вызов Abs на значении
	fmt.Println(p.Abs())     // OK: Go автоматически разыменует указатель
}

// Вывод:
// 5
// 5&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Важное уточнение: Это удобство работает только для методов. Если бы вы создали обычную функцию с аргументом-указателем, вам пришлось бы явно передавать указатель — Go не будет это делать автоматически.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Когда использовать какой получатель?&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;1. Когда нужна модификация&lt;/h3&gt;

&lt;p&gt;Если метод должен изменить получатель, необходимо использовать получателя-указатель:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h3&gt;2. Эффективность&lt;/h3&gt;

&lt;p&gt;Если получатель — большая структура, использование получателя-указателя более эффективно, так как избегает копирования данных при каждом вызове метода:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
type LargeStruct struct {
	Data [1000]float64
}

// Хорошо: получатель-указатель, избегаем копирования
func (ls *LargeStruct) Process() {
	// работаем со структурой
}&lt;/pre&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Консистентность&lt;/h2&gt;

&lt;p&gt;Важное правило: В общем случае все методы на выбранном типе должны иметь либо получателя-значение, либо получателя-указатель, но не смешивать оба. Это делает код предсказуемым и поддерживаемым.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Вот пример хорошей практики:&lt;/p&gt;

&lt;pre class="brush:cpp;"&gt;
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := &amp;amp;Vertex{3, 4}
	fmt.Printf("До масштабирования: %+v, длина: %v\n", v, v.Abs())
	v.Scale(5)
	fmt.Printf("После масштабирования: %+v, длина: %v\n", v, v.Abs())
}

// Вывод:
// До масштабирования: &amp;amp;{X:3 Y:4}, длина: 5
// После масштабирования: &amp;amp;{X:15 Y:20}, длина: 25&lt;/pre&gt;

&lt;p&gt;Обратите внимание, что оба метода используют получателя-указатель, что обеспечивает консистентность и позволяет методу Abs() эффективно работать с потенциально большими структурами.&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;h2&gt;Заключение: Чему мы научились?&lt;/h2&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;

&lt;p&gt;Методы в Go — это мощный инструмент для организации кода и создания чистых, читаемых программ. Вот ключевые моменты, которые мы разобрали:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1. Методы — это функции с получателем. Go предоставляет синтаксический сахар для вызова функций как методов, что улучшает читаемость.&lt;/p&gt;

&lt;p&gt;2. Выбор между значением и указателем очень важен. Получателя-указатель используйте, когда нужна модификация или когда структура большая. Получателя-значение используйте для небольших структур, которые не должны изменяться.&lt;/p&gt;

&lt;p&gt;3. Go предоставляет удобство разыменования. Методы можно вызывать как на значениях, так и на указателях, независимо от типа получателя.&lt;/p&gt;

&lt;p&gt;4. Консистентность важнее универсальности. Придерживайтесь одного стиля получателей для всех методов выбранного типа.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;br /&gt;
Методы позволяют писать объектно-ориентированный код, сохраняя при этом простоту и эффективность, за которые Go так ценится разработчиками по всему миру. Правильное использование методов и получателей сделает ваш код не только функциональным, но и удобным для других разработчиков, которые будут его поддерживать&lt;/p&gt;

&lt;p&gt;&lt;meta charset="UTF-8" /&gt;&lt;/p&gt;</summary>
    <dc:creator>Romo Fedoroff</dc:creator>
    <dc:date>2026-01-07T20:40:00Z</dc:date>
  </entry>
  <entry>
    <title>Apache NiFi: заметки по ведению логов потоков данных</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21198669" />
    <author>
      <name>Никита Рогаленко</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21198669</id>
    <updated>2025-12-29T14:16:23Z</updated>
    <published>2025-12-29T14:15:00Z</published>
    <summary type="html">&lt;p style="text-align: justify;"&gt;В этот позднедекабрьский день мы снова возвращаемся к Apache NiFi, чтобы в простой и компактной форме поделиться своим опытом и некоторыми практиками, связанными с организацией потока обработки данных. На этот раз проанализируем некоторые крайне важные особенности системы логирования событий в NiFi, а также то, как мы обычно в эти логи что-либо пишем.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;В первую очередь, вспомним, какие вообще бывают логи. В целом их 4 типа, хотя почти всегда нужен только один (nifi-app.log)&lt;/p&gt;

&lt;ul&gt;
	&lt;li style="text-align: justify;"&gt;nifi-app.log - сообщения от процессоров внутри потока, собственно главное средство для отладки потока и понимания того, что вообще происходит в потоке, что было передано, что получено, какие данные обработаны со стороны NiFi;&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;nifi-user.log - сообщения, связанные с событиями аутентификации и авторизации пользователей, аудит безопасности и проверка пользователей, открывающих веб-интерфейс NiFi;&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;nifi-bootstrap.log - сообщения о&amp;nbsp;запуске и остановке JVM, диагностика проблем с запуском и самой работоспособностью NiFi;&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;nifi-request.log - фиксация всех HTTP-запросов к NiFi, аудит веб-трафика (начиная с версии 1.16);&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;nifi-deprecation.log - логирование сведений об использовании устаревших процессоров (начиная с версии 1.18), используется для информации о наличии компонентов (или их отсутствии), препятствующих обновлению NiFi до новой версии (скажем, 2.x)&lt;/li&gt;
&lt;/ul&gt;

&lt;p style="text-align: justify;"&gt;Все эти логи хранятся вместе в одной директории (обычно в&amp;nbsp;/opt/nifi/nifi-current/logs, если NiFi развернут в Docker-контейнере, но зависит от специфики установки). Выглядит все это множество файлов например так:&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/20567281/logs-dir-ex.png/3c07f2d1-3623-2338-45d9-49bc201e4f63?imagePreview=1" /&gt;&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/20567281/more-logs.png/147f0dda-28c1-3a2b-9070-5fee822328bb?imagePreview=1" /&gt;&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Непосредственно файлы&amp;nbsp;nifi-app.log,&amp;nbsp;nifi-user.log,&amp;nbsp;nifi-bootstrap.log и&amp;nbsp;nifi-request.log представляют собой самые актуальные логи. Файлы же со временной отметкой являются логами историческими и описывают, что происходило с системой ранее. При этом важно четко осознавать, в какой момент логи сохраняются в файле с временной отметкой (то есть переходят в разряд прошедших событий) и каким образом NiFi понимает, что пора писать новый nifi-app.log файл. Далее мы будем рассматривать логирование в контексте nifi-app логов, поскольку их содержимое самое иформативное и интересует нас больше всего. Способы настройки логирования, о которых мы будет говорить далее, абсолютно идентичны и для остальных типов логов.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Итак, настройка же параметров логирования осуществляется в файле&amp;nbsp;/opt/nifi/nifi-current/conf/logback.xml. В данном файла нас наиболее интересует вот этот фрагмент:&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
&amp;lt;appender name="APP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"&amp;gt;
        &amp;lt;file&amp;gt;${org.apache.nifi.bootstrap.config.log.dir}/nifi-app.log&amp;lt;/file&amp;gt;
        &amp;lt;rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"&amp;gt;
            &amp;lt;!--
              For daily rollover, use 'app_%d.log'.
              For hourly rollover, use 'app_%d{yyyy-MM-dd_HH}.log'.
              To GZIP rolled files, replace '.log' with '.log.gz'.
              To ZIP rolled files, replace '.log' with '.log.zip'.
            --&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;${org.apache.nifi.bootstrap.config.log.dir}/nifi-app_%d.%i.log&amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;maxFileSize&amp;gt;100MB&amp;lt;/maxFileSize&amp;gt;
            &amp;lt;!-- keep 20 log files worth of history --&amp;gt;
            &amp;lt;maxHistory&amp;gt;20&amp;lt;/maxHistory&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
        &amp;lt;immediateFlush&amp;gt;true&amp;lt;/immediateFlush&amp;gt;
        &amp;lt;encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"&amp;gt;
            &amp;lt;pattern&amp;gt;%date %level [%thread] %logger{40} %msg%n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;Значения внутри тегов здесь указаны не такие, как по умолчанию, но это не так важно. Как можно увидеть, в атрибут name записано&amp;nbsp;"APP_FILE", а значит мы имеем дело с nifi-app.log, но с остальными типами все то же самое, конфигураторы для них расположены ниже в том же файле. При изменении любых настроек в этом файле не забудьте перезапустить NiFi для их применения.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Как мы видим, в файле уже есть комментарии, кратко поясняющие суть настройки логирования. Если в&amp;nbsp;fileNamePattern указать 'app_%d.log', то новый отдельный файл с логами будет появляться каждый день. Если укажем&amp;nbsp;'app_%d{yyyy-MM-dd_HH}.log', то каждый час. Также доступно архивирование в zip и gzip. Но кроме этого есть еще две важные характеристики -- это максимальный размер одного файла (maxFileSize) и максимальное число файлов для хранения (maxHistory). Подчеркнем, что в парадигме NiFi&lt;strong&gt; число хранимых файлов с логами определяется не конкретными временными рамками, а параметром, задающим максимальное количество файлов&lt;/strong&gt;. То есть мы храним логи не за "последние n дней", а строго в пределах максимально допустимого&amp;nbsp;maxHistory. Переход к записи логов в новый файл начнется либо когда наступит новый день/час в зависимости от настроек, либо когда размер одного файла превысит&amp;nbsp;maxFileSize. Обращу внимание, что в представленном выше фрагменте XML мы указали паттерн 'nifi-app_%d.%i.log', %i в нем означает индекс файла с логами. Это значит, что если в течение одного дня наберется больше 100МВ логов, то NiFi начнет писать логи уже в новый файл (на скриншотах выше можно заметить, например, файлы&amp;nbsp;nifi-app_2025-12-09.0.log,&amp;nbsp;nifi-app_2025-12-09.1.log и так далее).&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Таким образом, основный посыл этой заметки в том, что при менеджменте логов в NiFi-трансферах надо обращать внимание на размер одного файла, на максимальное их количество, на частоту добавления нового файла с логами. Все эти параметры сильно зависят от специфики проекта, ресурсов сервера, пожеланий заказчика. Важно понимать, что если, например, у вас&amp;nbsp;maxHistory равен 10, размер файла выставлен 50MB, а в один день данных внезапно пришло на 500МВ, то все предыдущие логи будут стерты и окажутся недоступны, что может быть проблемой, если необходимо проверить, что приходило в день предыдущий.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Обратим внимание, что в файле logback.xml также расположены уровни логирования для классов и пакетов Apache NiFi, где по желанию можно менять стандартные настройки, изменить WARN на INFO или на ERROR. Например:&lt;/p&gt;

&lt;pre class="brush:xml;"&gt;
&amp;lt;logger name="org.apache.nifi" level="INFO"/&amp;gt;
    &amp;lt;logger name="org.apache.nifi.processors" level="WARN"/&amp;gt;
    &amp;lt;logger name="org.apache.nifi.processors.standard.LogAttribute" level="INFO"/&amp;gt;
    &amp;lt;logger name="org.apache.nifi.processors.standard.LogMessage" level="INFO"/&amp;gt;
    &amp;lt;logger name="org.apache.nifi.controller.repository.StandardProcessSession" level="WARN" /&amp;gt;&lt;/pre&gt;

&lt;p style="text-align: justify;"&gt;Поднимаясь же на уровень выше и переходя непосредственно к потоку обработки данных, то есть в веб-интерфейс NiFi, то для логирования мы используем самый обычный процессор LogAttribute, который для этого и предназначен. Здесь можно разве что отдельно выделить параметр Log prefix, который мы всегда заполняем, чтобы обозначить, что вообще логируется в данный момент (факт, что NiFi получил запрос, что обработал данные, что успешно отправил что-либо куда нужно и т.п.). Log Payload указывает на то, логировать ли тело FlowFile'а, или же сохранять в логах только атрибуты. Для атрибутов также хорошей практикой будет выбирать не все значения, а задавать лишь самые важные в свойстве Attributes to Log. Так получится сэкономить место на диске и в целом сохранить больше логов.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;img height="373" src="https://www.tune-it.ru/documents/portlet_file_entry/20567281/logattribute.png/1d27556f-b5f5-92c6-94fc-3c443041017f?imagePreview=1" width="665" /&gt;&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;В конечном итоге информацию о прошедшем событии можно будет посмотреть в файле nifi-app.log (или в более поздних app файлах). Пример выше довольно простой, в нем мы предполагаем, что у нас NiFi получает запрос одного вида с параметрами ИД и тип, которые мы и хотим залогировать. Добавив процессор выше, мы обнаружим в логах нечто вроде указанного ниже, где префикс будет разделять нужную информацию с остальными логами:&lt;/p&gt;

&lt;pre class="brush:plain;"&gt;
------------[RECORD REQUEST RECEIVED]-------------
FlowFile Properties
Key: 'entryDate'
	Value: 'Thu Dec 18 15:19:17 UTC 2025'
Key: 'lineageStartDate'
	Value: 'Thu Dec 18 15:19:17 UTC 2025'
Key: 'fileSize'
	Value: '0'
FlowFile Attribute Map Content
Key: 'http.query.param.id'
	Value: '12375823'
Key: 'http.query.param.type'
	Value: 'Type1'
------------[RECORD REQUEST RECEIVED]-------------&lt;/pre&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Никита Рогаленко</dc:creator>
    <dc:date>2025-12-29T14:15:00Z</dc:date>
  </entry>
  <entry>
    <title>Как проектировать интерфейс, если ты не дизайнер. Базовые правила хорошего дизайна.</title>
    <link rel="alternate" href="https://www.tune-it.ru/c/blogs/find_entry?entryId=21198094" />
    <author>
      <name>Алексей Кондратьев</name>
    </author>
    <id>https://www.tune-it.ru/c/blogs/find_entry?entryId=21198094</id>
    <updated>2025-12-27T20:52:06Z</updated>
    <published>2025-12-27T20:33:00Z</published>
    <summary type="html">&lt;p&gt;В мире стартапов и быстрой разработки часто возникает ситуация: продукт должен быть создан вчера, бюджет ограничен, а в команде нет профессионального дизайнера. Разработчики, менеджеры продуктов, даже основатели вынуждены брать на себя роль дизайнера.&amp;nbsp;&lt;br /&gt;
В этом вынужденном самообразовании, конечно, есть своя прелесть и практическая ценность, однако зачастую им пренебрегают, создавая куцые и хаотичные визуалы, путающие пользователя. Понимание основ дизайна — это новый язык, на котором можно говорить с пользователями, и овладеть им стоит каждому, кто создает цифровые продукты. Это руководство поможет осознать и научиться применять базовые принципы дизайна, которые помогут создать хороший и чистый дизайн без большого количества практики.&lt;/p&gt;

&lt;h1&gt;&lt;br /&gt;
Главное правило: Простого достаточно&lt;/h1&gt;

&lt;p&gt;Первый и самый важный принцип дизайна, если вы не дизайнер — не навреди. Часто стремление сделать «красиво» приводит к перегруженным, непрактичным интерфейсам. Помните: лучший дизайн часто тот, который не замечают. Когда пользователь интуитивно понимает, как взаимодействовать с интерфейсом, не задумываясь о его «дизайне», — вы на правильном пути.&lt;/p&gt;

&lt;p&gt;Эта философия находит свое выражение в минимализме, который стал не просто трендом, а практической необходимостью. Каждый лишний элемент — это когнитивная нагрузка, это вопрос, который пользователь должен решить: «Что это? Зачем это? Нужно ли мне это?»&lt;br /&gt;
Хотите, чтобы пользователь совершил нужное вам действие — не отвлекайте его.&amp;nbsp;&lt;/p&gt;

&lt;h1&gt;Второе правило: не изобретайте велосипед.&lt;/h1&gt;

&lt;p&gt;Мир digital-дизайна уже прошел долгий путь методом проб и ошибок, и плоды этого опыта доступны каждому. Используйте готовые дизайн-системы, привычные паттерны страниц и уже известные заголовки. Поверьте, пользователь скажет вам спасибо.&lt;/p&gt;

&lt;p&gt;Современные дизайн-системы — это готовые библиотеки компонентов, созданные и протестированные крупнейшими компаниями:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Material Design (Google) &lt;/strong&gt;— философия материальных поверхностей, реалистичных теней и осмысленной анимации. Это целая экосистема с четкими правилами и компонентами для любой ситуации.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Human Interface Guidelines (Apple)&lt;/strong&gt; — минималистичный, воздушный подход с акцентом на ясность и уважение к контенту. Особенно важен для iOS-приложений.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Fluent Design (Microsoft)&lt;/strong&gt; — система, построенная вокруг концепции «света, глубины, движения, материала и масштаба».&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Использование этих систем — самое разумное решение, если вы впервые создаете дизайн. Ваши пользователи уже знакомы с этими компонентами и интуитивно понимают, что к чему. Это сокращает время обучения и уменьшает количество ошибок.&lt;/p&gt;

&lt;p&gt;Однако ваша задача — не слепо копировать, а адаптировать. Возьмите базовые компоненты, но окрасьте их в цвета вашего бренда. Сохраните логику взаимодействия, но измените визуальную выразительность там, где это важно для уникальности продукта.&lt;/p&gt;

&lt;h1&gt;Третье правило: цвета и иерархия&lt;/h1&gt;

&lt;p&gt;Парадокс дизайна для не-дизайнеров заключается в том, что ваша главная сила — в признании своих ограничений. Вы не профессиональный дизайнер, и это освобождает вас от необходимости создавать что-то «дизайнерское».&lt;br /&gt;
Используйте ограниченное и конкретное число цветов и размеров шрифтов, так будет меньше шансов создать хаос.&lt;/p&gt;

&lt;h2&gt;Правило трех цветов&lt;/h2&gt;

&lt;p&gt;Выберите:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Основной цвет (для ключевых действий, акцентов)&lt;/li&gt;
	&lt;li&gt;Нейтральный цвет (для фона, основного текста)&lt;/li&gt;
	&lt;li&gt;Цвет для состояния (успех, ошибка, предупреждение)&lt;/li&gt;
&lt;/ul&gt;

&lt;p dir="ltr"&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21107385/Screenshot+from+2025-12-27+23-38-37.png/316e2036-3b9d-6586-31be-a62313e9df04?imagePreview=1" style="height: 200px;" /&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;b id="docs-internal-guid-c216b673-7fff-766e-e400-22f36034e86e"&gt;Этого достаточно для 90% интерфейсов.&lt;/b&gt;&lt;/p&gt;

&lt;h2 dir="ltr"&gt;&lt;b id="docs-internal-guid-c216b673-7fff-766e-e400-22f36034e86e"&gt;Шрифтовая иерархия&lt;/b&gt;&lt;/h2&gt;

&lt;p&gt;Задайте себе три вопроса о каждом элементе интерфейса:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;Это критически важно для выполнения основной задачи?&lt;/li&gt;
	&lt;li&gt;Это полезная дополнительная информация?&lt;/li&gt;
	&lt;li&gt;Это нужно вообще?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Располагайте элементы в соответствии с этим.&lt;br /&gt;
&lt;u&gt;Заголовки — самые важные и самые большие. Далее стандартный основной текст и чуть меньше его — подсказки.&lt;/u&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21107385/Screenshot+from+2025-12-27+23-39-22.png/70610c03-e332-1bf6-59eb-fa2a73ef76c5?imagePreview=1" style="width: 320px; height: 264px;" /&gt;&lt;/p&gt;

&lt;p dir="ltr"&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;Общая иерархия&lt;/h2&gt;

&lt;p&gt;Не забывайте также о том, что расположение всех элементов интерфейса должно подчиняться логике. Связанные по смыслу блоки должны располагаться друг к другу ближе, чем ко всем остальным элементам. Например, заголовок должен быть ближе к тексту и кнопке, нежели к остальным элементам, а расстояние между абзацами должно быть меньше, чем расстояние между заголовком и текстом.&amp;nbsp;&lt;/p&gt;

&lt;p dir="ltr"&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21107385/Screenshot+from+2025-12-27+23-46-34.png/fbc8e5d7-25b8-8ff9-7274-8e3bbf68f761?imagePreview=1" style="width: 660px; height: 297px;" /&gt;&lt;br /&gt;
&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21107385/Screenshot+from+2025-12-27+23-40-00.png/95f17887-aa76-69d5-d537-ff9c959b6c39?imagePreview=1" style="width: 660px; height: 278px;" /&gt;&lt;/p&gt;

&lt;p&gt;Наш мозг неосознанно соединяет рядом стоящие объекты и если нарушить правила композиции, то возникнет искажение (как на втором примере)&lt;/p&gt;

&lt;div class="portlet-msg-info"&gt;Совет: Используйте модульные размеры, чтобы не придумывать каждый отступ заново. Модульные размеры — это числа, кратные, например, 8px. Таким образом, расстояния между объектами будут 8, 16, 24, 32 и тд. Для упрощения часто используют только несколько самых важных: 8, 16, 32, 64, 80/96 и 128&lt;/div&gt;

&lt;h2&gt;&lt;br /&gt;
Автолейаут для чайников&lt;/h2&gt;

&lt;p&gt;Для тех, кто создает интерфейсы, одна из самых революционных концепций последних лет — автоматическая верстка (autolayout в Figma, аналог flexbox в вебе). Если в нем разобраться, он сильно упростит работу с дизайном.&lt;/p&gt;

&lt;h4&gt;Практическое руководство по автолейауту&lt;/h4&gt;

&lt;ol&gt;
	&lt;li&gt;Начните с фрейма — в Figma это основа для автолейаута&lt;/li&gt;
	&lt;li&gt;Выберите направление — горизонтальное (ряд) или вертикальное (колонка)&lt;/li&gt;
	&lt;li&gt;Определите поведение при растягивании — должен ли элемент растягиваться или сохранять размер?&lt;/li&gt;
	&lt;li&gt;Настройте отступы (padding) — внутренние отступы от краев фрейма&lt;/li&gt;
	&lt;li&gt;Установите расстояние между элементами (gap)&lt;/li&gt;
&lt;/ol&gt;

&lt;p dir="ltr"&gt;&lt;img src="https://www.tune-it.ru/documents/portlet_file_entry/21107385/Screenshot+from+2025-12-27+23-40-10.png/2d305458-8db6-1b05-fecb-76933de9ebdd?imagePreview=1" style="height: 262px; width: 900px;" /&gt;&lt;/p&gt;

&lt;p&gt;Волшебство происходит, когда вы меняете текст: вся карточка автоматически перестраивается, сохраняя правильные отступы. Вы создаете не статичный макет, а живую систему, которая адаптируется к контенту.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;Эмоциональный интеллект интерфейса&lt;/h2&gt;

&lt;p&gt;Даже самый технически совершенный интерфейс может провалиться, если он не учитывает человеческий фактор. Вот где проявляется разница между «сделанным» и «продуманным» дизайном.&lt;/p&gt;

&lt;h4&gt;Микроинтеракции как проявление заботы&lt;/h4&gt;

&lt;p&gt;Небольшие анимации, изменения состояний, звуковая обратная связь — это элементы, которые превращают функциональный инструмент в приятный опыт. Кнопка, которая слегка «нажимается» при клике, поле ввода, которое подсвечивается при фокусе, плавная подгрузка контента — эти детали создают ощущение качества.&lt;/p&gt;

&lt;div class="portlet-msg-info"&gt;Практический совет: Используйте стандартные, предсказуемые анимации. Не стоит делать выпадающее меню, которое выезжает по диагонали с вращением — это сбивает с толку. Лучше плавное появление сверху вниз за 300 миллисекунд.&lt;/div&gt;

&lt;h4&gt;Язык, который говорит с пользователем&lt;/h4&gt;

&lt;p&gt;Текст в интерфейсе — это не просто информация, это голос вашего продукта. Сообщения об ошибках, подсказки, призывы к действию — всё это формирует отношение пользователя.&lt;br /&gt;
Вместо сухого «Ошибка 404» напишите «Кажется, мы потеряли эту страницу. Попробуйте начать с главной». Вместо «Отправить» на кнопке формы — «Сохранить изменения» или «Завершить настройку».&lt;/p&gt;

&lt;p&gt;Однако, не стоит забывать, что любые тексты в интерфейсе должны соответствовать вашему Tone of Voice. О нем я писал &lt;a href="https://www.tune-it.ru/web/anpan/blog/-/blogs/govorite-na-odnom-azyke-so-svoimi-klientami-cto-takoe-tone-of-voice-i-kak-ego-najti-"&gt;в этой статье&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;Заключение: Почему с дизайнером всё же лучше?&lt;/h2&gt;

&lt;p&gt;И вот мы подходим к важному признанию: всё, о чем мы говорили, — это искусство создания достаточно хорошего интерфейса в условиях ограниченных ресурсов. Но между «достаточно хорошим» и «качественным» лежит пропасть, которую может преодолеть только профессионал.&lt;/p&gt;

&lt;h4&gt;Что на самом деле делает дизайнер&lt;/h4&gt;

&lt;p&gt;Профессиональный дизайнер — это не просто человек, который «умеет в Figma». Это:&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Переводчик между человеком и машиной &lt;/strong&gt;— он понимает, как люди воспринимают информацию, и преобразует функциональные требования в интуитивные интерфейсы.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Системный мыслитель&lt;/strong&gt; — он создает не набор экранов, а целостную экосистему, где каждый компонент логически связан с другими.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Эмоциональный архитектор&lt;/strong&gt; — он строит не просто пользовательский путь, а эмоциональное путешествие, где каждая точка взаимодействия продумана для создания определенного настроения.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Инноватор&lt;/strong&gt; — в то время как не-дизайнеры опираются на существующие паттерны, дизайнеры создают новые, расширяя саму концепцию того, каким может быть взаимодействие с технологиями.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Хороший дизайн также сокращает время разработки — продуманная система компонентов позволяет разрабатывать новые функции в 2-3 раза быстрее. И снижает стоимость поддержки — интуитивный интерфейс уменьшает количество обращений в поддержку и ошибок пользователей.&lt;/p&gt;

&lt;p&gt;Лучшие цифровые продукты рождаются не тогда, когда дизайнер заменяет разработчика или наоборот, а когда они работают в симбиозе. Разработчик приносит понимание технических возможностей и ограничений. Дизайнер приносит понимание человеческого восприятия и поведения. Вместе они создают то, что невозможно создать по отдельности.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Потому что в конечном счете, хороший дизайн — это не про красивые картинки. Это про уважение к времени, вниманию и разуму пользователя. И это достойно того, чтобы доверить это профессионалу.&lt;/em&gt;&lt;br /&gt;
&amp;nbsp;&lt;/p&gt;</summary>
    <dc:creator>Алексей Кондратьев</dc:creator>
    <dc:date>2025-12-27T20:33:00Z</dc:date>
  </entry>
</feed>

