<?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-05-06T10:00:53Z</updated>
  <dc:date>2026-05-06T10:00:53Z</dc:date>
  <entry>
    <title>Astro.js: современный фреймворк для контент-ориентированных сайтов</title>
    <link rel="alternate" href="https://www.tune-it.ru/en/c/blogs/find_entry?entryId=21251834" />
    <author>
      <name>Vadim Mikhu</name>
    </author>
    <id>https://www.tune-it.ru/en/c/blogs/find_entry?entryId=21251834</id>
    <updated>2026-05-06T09:50:23Z</updated>
    <published>2026-05-06T09:23:00Z</published>
    <summary type="html">&lt;h1&gt;Astro.js: современный фреймворк для контент-ориентированных сайтов&lt;/h1&gt;

&lt;h2&gt;Что такое Astro и зачем он нужен&lt;/h2&gt;

&lt;p data-end="451" data-start="57"&gt;Astro — это фреймворк для сайтов и веб-приложений, который по умолчанию рендерит большую часть страницы в статический HTML на сервере, а JavaScript подключает только там, где он действительно нужен. Такой подход называется islands architecture: основная часть страницы остается «легкой», а интерактивность добавляется небольшими изолированными фрагментами.&lt;/p&gt;

&lt;p data-end="752" data-start="453"&gt;Astro поддерживает несколько UI-фреймворков одновременно: React, Preact, Vue, Svelte, Solid и веб-компоненты. Это делает его удобным, когда нужно сочетать контентные страницы, маркетинговые лендинги, документацию и отдельные интерактивные блоки в одном проекте.&lt;/p&gt;

&lt;h2&gt;Ключевые преимущества&lt;/h2&gt;

&lt;h3&gt;Производительность «из коробки»&lt;/h3&gt;

&lt;p&gt;Astro проектировался как server-first решение, поэтому большинство страниц может отдаваться без необходимости тащить на клиент целый фреймворк runtime. В документации прямо отмечено, что это снижает сложность по сравнению с другими UI-фреймворками, потому что на сервере нет необходимости управлять реактивностью, хуками, stale closures и похожими механизмами.&lt;/p&gt;

&lt;h3&gt;UI-агностичность&lt;/h3&gt;

&lt;p&gt;Astro имеет систему интеграций и renderer-ов, через которую подключаются UI-фреймворки, адаптеры для серверного рендеринга и дополнительные возможности вроде MDX или sitemap. Для обычного проекта подключение официальной интеграции делается в несколько строк.&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;React&lt;/strong&gt; — для компонентов с богатым состоянием,&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Preact&lt;/strong&gt; — там, где нужен React-подобный синтаксис с минимальным весом,&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Svelte&lt;/strong&gt; — для компактных и производительных виджетов,&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Vue&lt;/strong&gt; — если команда привыкла к его экосистеме,&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Lit&lt;/strong&gt; — для веб-компонентов.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;На практике это особенно ценно при миграции старых проектов: вы можете постепенно переписывать компоненты, не ломая всё сразу.&lt;/p&gt;

&lt;h3&gt;Гибкая модель интерактивности&lt;/h3&gt;

&lt;p&gt;Astro использует компонентный подход, но интерактивность включается точечно через hydration directives: &lt;code data-end="1403" data-start="1390"&gt;client:load&lt;/code&gt;, &lt;code data-end="1418" data-start="1405"&gt;client:idle&lt;/code&gt;, &lt;code data-end="1436" data-start="1420"&gt;client:visible&lt;/code&gt;, &lt;code data-end="1452" data-start="1438"&gt;client:media&lt;/code&gt; и &lt;code data-end="1468" data-start="1455"&gt;client:only&lt;/code&gt;. Это позволяет запускать JavaScript только для конкретных элементов интерфейса и выбирать момент загрузки в зависимости от приоритета UI.&lt;/p&gt;

&lt;h3&gt;Content Collections&lt;/h3&gt;

&lt;p&gt;Начиная с версии 2.0, Astro предлагает типизированные коллекции контента — встроенную систему для работы с Markdown, MDX и другими форматами с полной поддержкой TypeScript.&lt;/p&gt;

&lt;h2&gt;Технические основы: как это работает&lt;/h2&gt;

&lt;h3&gt;Этапы сборки&lt;/h3&gt;

&lt;p&gt;Когда вы запускаете &lt;code&gt;astro build&lt;/code&gt;, происходит следующее:&lt;/p&gt;

&lt;ol&gt;
	&lt;li&gt;&lt;strong&gt;Парсинг компонентов&lt;/strong&gt; — Astro читает все &lt;code&gt;.astro&lt;/code&gt;-файлы и компоненты UI-фреймворков.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Статическая генерация&lt;/strong&gt; — выполняется JavaScript в &lt;code&gt;frontmatter&lt;/code&gt; (часть между &lt;code&gt;---&lt;/code&gt;), которая может делать запросы к API, читать файлы и т.д.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Рендеринг в HTML&lt;/strong&gt; — вся разметка превращается в чистый HTML.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Выделение «островов»&lt;/strong&gt; — компоненты с клиентскими директивами (&lt;code&gt;client:load&lt;/code&gt;, &lt;code&gt;client:idle&lt;/code&gt; и т.д.) упаковываются отдельно.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Гидратация&lt;/strong&gt; — при загрузке страницы браузер загружает только нужные острова, причём строго по заданной стратегии.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;Клиентские директивы&lt;/h3&gt;

&lt;p&gt;Директивы определяют, &lt;strong&gt;когда&lt;/strong&gt; браузер должен&amp;nbsp;оживить&amp;nbsp;компонент:&lt;/p&gt;

&lt;table&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th&gt;Директива&lt;/th&gt;
			&lt;th&gt;Поведение&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;client:load&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;Немедленно при загрузке страницы&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;client:idle&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;Когда браузер освободился (через &lt;code&gt;requestIdleCallback&lt;/code&gt;)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;client:visible&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;Когда компонент попадает в область видимости (Intersection Observer)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;client:media="(max-width: 768px)"&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;При совпадении медиазапроса&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;client:only="preact"&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;Только на клиенте, без серверного рендеринга&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;Синтаксис &lt;code&gt;.astro&lt;/code&gt;-файлов&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.astro&lt;/code&gt;-файл делится на две части: &lt;strong&gt;frontmatter&lt;/strong&gt; (серверный JavaScript) и &lt;strong&gt;шаблон&lt;/strong&gt; (HTML-подобная разметка).&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;---
// Всё здесь выполняется ТОЛЬКО на сервере/в момент сборки.
// Этот код никогда не попадёт в браузер.

import Header from '../components/Header.astro';
import ProductCard from '../components/ProductCard.jsx'; // React-компонент
import { getCollection } from 'astro:content';

const products = await fetch('https://api.example.com/products')
  .then(res =&amp;gt; res.json());

const { title = 'Каталог товаров' } = Astro.props;
---

&amp;lt;html lang="ru"&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset="UTF-8" /&amp;gt;
    &amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;!-- Статический Astro-компонент --&amp;gt;
    &amp;lt;Header /&amp;gt;

    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;{title}&amp;lt;/h1&amp;gt;

      &amp;lt;ul class="product-grid"&amp;gt;
        {products.map(product =&amp;gt; (
          &amp;lt;!-- ProductCard — React-компонент --&amp;gt;
          &amp;lt;ProductCard client:visible name={product.name}/&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/main&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;style&amp;gt;...&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Astro автоматически изолирует CSS-стили — каждый &lt;code&gt;.astro&lt;/code&gt;-файл получает уникальный хэш-атрибут.&lt;/p&gt;

&lt;h3&gt;Layouts — переиспользуемые макеты&lt;/h3&gt;

&lt;p&gt;В Astro макеты — это просто компоненты со слотом &lt;code&gt;&amp;lt;slot /&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;---
// src/layouts/BaseLayout.astro
interface Props {
  title: string;
  description?: string;
}

const { title, description = 'Мой Astro-сайт' } = Astro.props;
---

&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang="ru"&amp;gt;
  &amp;lt;head&amp;gt;
    ...
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;nav class="site-nav"&amp;gt; ... &amp;lt;/nav&amp;gt;

    &amp;lt;!-- Содержимое страницы подставляется сюда --&amp;gt;
    &amp;lt;slot /&amp;gt;

    &amp;lt;footer&amp;gt;© 2024 Мой сайт&amp;lt;/footer&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Маршрутизация и работа с данными&lt;/h2&gt;

&lt;h3&gt;Файловая маршрутизация&lt;/h3&gt;

&lt;p&gt;Astro использует файловую систему для маршрутизации — принцип, знакомый по Next.js:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;src/pages/
├── index.astro          → /
├── about.astro          → /about
├── blog/
│   ├── index.astro      → /blog
│   └── [slug].astro     → /blog/любой-slug
└── api/
    └── products.ts      → /api/products  (API-эндпоинт)
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Динамические маршруты и &lt;code&gt;getStaticPaths&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Для генерации динамических страниц используется функция &lt;code&gt;getStaticPaths&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

// Эта функция говорит Astro: "вот все возможные URL, которые нужно сгенерировать"
export async function getStaticPaths() {
  const posts = await getCollection('blog'); // читаем коллекцию из src/content/blog/

  return posts.map(post =&amp;gt; ({
    params: { slug: post.slug },
    props: { post },              // передаём данные в компонент
  }));
}

const { post } = Astro.props;
const { Content } = await post.render(); // превращаем Markdown в компонент
---

&amp;lt;BlogLayout title={post.data.title} description={post.data.description}&amp;gt;
  &amp;lt;article&amp;gt;
    &amp;lt;h1&amp;gt;{post.data.title}&amp;lt;/h1&amp;gt;
    &amp;lt;time&amp;gt;{post.data.publishedAt.toLocaleDateString('ru-RU')}&amp;lt;/time&amp;gt;
    &amp;lt;!-- Content — это готовый к рендерингу HTML из Markdown-файла --&amp;gt;
    &amp;lt;Content /&amp;gt;
  &amp;lt;/article&amp;gt;
&amp;lt;/BlogLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Content Collections с типизацией&lt;/h3&gt;

&lt;p&gt;Система коллекций позволяет описать схему frontmatter и получить полную автодополнение в редакторе:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content', // 'content' для Markdown, 'data' для JSON/YAML
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishedAt: z.date(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    // Если поля нет — TypeScript немедленно об этом сообщит
    coverImage: z.string().optional(),
  }),
});

export const collections = {
  blog: blogCollection,
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;После этого &lt;code&gt;getCollection('blog')&lt;/code&gt; вернёт типизированный массив, и вы не сможете случайно обратиться к несуществующему полю.&lt;/p&gt;

&lt;h3&gt;API-эндпоинты&lt;/h3&gt;

&lt;p&gt;Astro позволяет создавать серверные API прямо внутри проекта:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;// src/pages/api/newsletter.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) =&amp;gt; {
  const body = await request.json();
  const { email } = body;

  if (!email || !email.includes('@')) {
    return new Response(
      JSON.stringify({ error: 'Некорректный email' }),
      { status: 400, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Здесь можно вызвать любой сервис: Mailchimp, Resend, собственную БД...
  await subscribeToNewsletter(email);

  return new Response(
    JSON.stringify({ success: true }),
    { status: 200, headers: { 'Content-Type': 'application/json' } }
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Сравнение с другими фреймворками&lt;/h2&gt;

&lt;p&gt;Понять место Astro в экосистеме проще всего через сравнение с конкурентами. Но важно понимать: это не «Astro против Next.js», а «разные инструменты для разных задач».&lt;/p&gt;

&lt;h3&gt;Сводная таблица&lt;/h3&gt;

&lt;table&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th&gt;Критерий&lt;/th&gt;
			&lt;th&gt;Astro&lt;/th&gt;
			&lt;th&gt;Next.js&lt;/th&gt;
			&lt;th&gt;SvelteKit&lt;/th&gt;
			&lt;th&gt;Nuxt&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;JS по умолчанию&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;/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;td&gt;❌ Нет&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;SSR&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;/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;td&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;Rich SPA&lt;/td&gt;
			&lt;td&gt;Svelte-приложения&lt;/td&gt;
			&lt;td&gt;Vue-приложения&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;td&gt;Средний&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;Интеграция с UI-фреймворками на практике: Preact&lt;/h2&gt;

&lt;p data-end="5380" data-start="4963"&gt;Preact — это компактная библиотека компонентов, совместимая с React-экосистемой через &lt;code data-end="5064" data-start="5049"&gt;preact/compat&lt;/code&gt;. В официальных материалах Preact подчеркивается, что compatibility layer позволяет использовать React-компоненты как drop-in replacement в ряде случаев, а сам Preact отличается более близким к DOM поведением и меньшим runtime-overhead, в том числе без synthetic event system.&lt;/p&gt;

&lt;p data-end="5664" data-start="5382"&gt;В Astro интеграция с Preact подключается официальным пакетом &lt;code data-end="5460" data-start="5443"&gt;@astrojs/preact&lt;/code&gt;. Для обычного использования достаточно команды &lt;code data-end="5526" data-start="5508"&gt;astro add preact&lt;/code&gt;; после этого Astro умеет рендерить Preact-компоненты и гидрировать их через &lt;code data-end="5613" data-start="5603"&gt;client:*&lt;/code&gt; директивы.&lt;/p&gt;

&lt;h3&gt;Установка и конфигурация&lt;/h3&gt;

&lt;pre&gt;
&lt;code&gt;# Добавляем интеграцию одной командой
npx astro add preact

# Или вручную через npm
npm install @astrojs/preact preact
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;После этого обновляем конфиг:&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;// astro.config.mjs
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';

export default defineConfig({
  integrations: [
    preact({
      // compat: true — включает слой совместимости с React,
      // что позволяет использовать некоторые React-библиотеки с Preact
      compat: true,
    }),
  ],
});&lt;/code&gt;
&lt;/pre&gt;

&lt;h3&gt;Особенности и тонкости при работе с Preact в Astro&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Атрибуты &lt;code&gt;class&lt;/code&gt; vs &lt;code&gt;className&lt;/code&gt;.&lt;/strong&gt; В Astro-компонентах (&lt;code&gt;.astro&lt;/code&gt;) всегда используется &lt;code&gt;class&lt;/code&gt;. В Preact-компонентах (&lt;code&gt;.jsx&lt;/code&gt;) тоже рекомендуется &lt;code&gt;class&lt;/code&gt;, а не &lt;code&gt;className&lt;/code&gt; — это одно из отличий Preact от React.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Серверный рендеринг Preact-компонентов.&lt;/strong&gt; Даже без клиентской директивы Preact-компоненты рендерятся на сервере в HTML. Директива лишь добавляет гидратацию. Это значит, что поисковик увидит контент компонента, даже если JavaScript в браузере отключён.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;client:only="preact"&lt;/code&gt;&lt;/strong&gt; нужен в редких случаях — когда компонент использует браузерные API (localStorage, window) и не может быть отрендерен на сервере. В таком случае Astro пропускает серверный рендеринг полностью.&lt;/p&gt;

&lt;pre&gt;
&lt;code&gt;// Компонент, который читает из localStorage — не может работать на сервере
export default function ThemeToggle() {
  const [theme, setTheme] = useState(
    // localStorage доступен только в браузере
    () =&amp;gt; localStorage.getItem('theme') ?? 'light'
  );

  // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;
&lt;code&gt;&amp;lt;!-- Правильно: client:only пропускает SSR --&amp;gt;
&amp;lt;ThemeToggle client:only="preact" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Итог&lt;/h2&gt;

&lt;p&gt;Astro полезен там, где важны скорость первой отрисовки, небольшой клиентский бандл и гибкая интеграция с несколькими UI-фреймворками. Preact в этой экосистеме хорошо подходит для компактных интерактивных компонентов и для проектов, где нужна совместимость с частью React-экосистемы, но без полного веса React runtime.&lt;/p&gt;</summary>
    <dc:creator>Vadim Mikhu</dc:creator>
    <dc:date>2026-05-06T09:23:00Z</dc:date>
  </entry>
  <entry>
    <title>Введение в архитектуру ПО. Часть 1: Истинная ценность структуры и цена спешки</title>
    <link rel="alternate" href="https://www.tune-it.ru/en/c/blogs/find_entry?entryId=21249903" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/en/c/blogs/find_entry?entryId=21249903</id>
    <updated>2026-05-01T13:23:55Z</updated>
    <published>2026-05-01T12: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 style="text-align: justify;"&gt;Введение&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Вы когда-нибудь сталкивались с ситуацией, когда добавление «одной небольшой кнопки» на сайт превращается в задачу на несколько дней? Или когда каждый новый запрос от бизнеса оказывается сложнее предыдущего, хотя на первый взгляд всё выглядит просто? Как правило, причина кроется не в уровне команды и не в сложности самих задач -&amp;nbsp;а в том, на каком фундаменте выполнен проект.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Давайте разберёмся: зачем нужна архитектура, почему спешка в начале выходит боком впоследствии, и как выстраивать конструктивный диалог с руководством о качестве кода.&lt;/p&gt;

&lt;h2 style="text-align: justify;"&gt;Что на самом деле означает хорошая архитектура&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Распространённое заблуждение -&amp;nbsp;считать, что хорошая архитектура обязательно предполагает сложные паттерны, многоуровневые абстракции или модные технологические решения. На практике всё значительно проще.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p style="text-align: justify;"&gt;Хорошая архитектура -&amp;nbsp;это когда разработчики тратят минимум усилий на создание нового функционала и поддержку существующего.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Именно это и является единственным значимым критерием.&lt;/strong&gt;&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Если добавление новой возможности занимает примерно одинаковое время независимо от того, на каком этапе жизни находится проект -&amp;nbsp;архитектура выстроена правильно. Если с каждым релизом задачи становятся всё сложнее, сроки всё менее предсказуемы, а команда всё больше времени тратит на разбор уже написанного кода -&amp;nbsp;проблема заложена в самом основании системы.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Для тех, кто только начинает путь в разработке:&lt;/strong&gt;&amp;nbsp;представьте конструктор. Если детали рассортированы по типам и размерам, собирать новые уровни легко. Если всё свалено в одну кучу, а нижние уровни собраны кое-как -&amp;nbsp;каждый новый этаж будет даваться труднее предыдущего, потому что прежде всего придётся укреплять то, что уже есть.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Для специалистов с опытом:&amp;nbsp;&lt;/strong&gt;в терминологии бизнеса это совокупная стоимость владения продуктом. Слабая архитектура делает кривую стоимости изменений экспоненциальной. Когда вы обосновываете необходимость рефакторинга перед заинтересованными сторонами - говорите именно об этом, а не об абстрактном «качестве кода».&lt;/p&gt;

&lt;h2 style="text-align: justify;"&gt;Синдром быстрого старта и иллюзия «разберёмся потом»&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Представьте строительную бригаду, которой поставили задачу возвести жилой дом в сжатые сроки. Чтобы уложиться в дедлайн, они принимают решение не тратить время на проектирование фундамента по всем нормам -&amp;nbsp;заливают его быстро, приблизительно, «и так сойдёт». Первый этаж встаёт быстро. Второй - тоже. Заказчик доволен, темп впечатляет.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Но уже к третьему этажу стены начинают давать трещины. Четвёртый этаж строить страшно - фундамент не рассчитан на такую нагрузку. Чтобы продолжить строительство, бригаде приходится остановиться и укреплять основание -&amp;nbsp;то самое, на которое не захотели тратить время в самом начале. В итоге работы занимают вдвое больше времени, чем если бы фундамент был заложен правильно с первого дня.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;В разработке программного обеспечения происходит ровно то же самое.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p style="text-align: justify;"&gt;Команды работают в условиях постоянного давления: сроки поджимают, рынок не ждёт. В такой обстановке легко поддаться соблазну сделать «как-нибудь сейчас, а разберёмся потом». Это, пожалуй, одно из самых распространённых и дорогостоящих заблуждений в индустрии.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Проблема в том, что это «потом» почти никогда не наступает. После первой задачи приходит вторая, за ней третья -&amp;nbsp;давление не ослабевает, а технический долг неуклонно накапливается. До тех пор, пока рутинная задача вдруг не начинает занимать неделю вместо дня.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p style="text-align: justify;"&gt;Именно тогда возникает идея: «Давайте перепишем всё с нуля». Но если подход к проектированию не меняется по существу, новая система через несколько лет придёт ровно к тому же результату -&amp;nbsp;как если бы та же бригада построила рядом новый дом, но снова без нормального фундамента.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Здесь работает простая закономерность: поддерживать порядок в коде с самого начала - быстрее и дешевле, чем разгребать последствия накопленного беспорядка. На любой дистанции и при любом масштабе проекта.&lt;/p&gt;

&lt;h2 style="text-align: justify;"&gt;Технический долг: когда метафора становится реальностью&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Понятие технического долга -&amp;nbsp;одно из наиболее точных в профессиональном лексиконе разработчиков. Оно появилось не случайно: его автор, программист Уорд Каннингем, намеренно использовал финансовую аналогию, чтобы сделать проблему понятной для бизнеса.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Суть аналогии такова. Когда компания берёт кредит, она получает деньги сейчас, но обязуется выплачивать проценты в будущем. Чем дольше она тянет с погашением -&amp;nbsp;тем больше итоговая переплата. Технический долг работает по той же логике: принимая решение срезать углы сегодня, команда как бы берёт кредит у будущего. Расплата приходит в виде всё возрастающих затрат на поддержку и развитие системы.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p style="text-align: justify;"&gt;Важно понимать: технический долг сам по себе не всегда является ошибкой. Иногда это осознанное и обоснованное решение. Например, стартап на ранней стадии может намеренно выбрать более простое техническое решение, чтобы быстро проверить гипотезу на рынке. Это разумно -&amp;nbsp;при условии, что долг фиксируется явно и план по его погашению существует.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p style="text-align: justify;"&gt;Проблема возникает тогда, когда технический долг накапливается неосознанно -&amp;nbsp;когда команда не отдаёт себе отчёта в том, что именно она делает и какие последствия это повлечёт. В таком случае «проценты» начинают начисляться сами собой, и в какой-то момент их обслуживание поглощает большую часть производительности команды.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Признаки того, что технический долг вышел из-под контроля, как правило, очевидны:&lt;/p&gt;

&lt;ul&gt;
	&lt;li style="text-align: justify;"&gt;Разработчики боятся вносить изменения в определённые части системы, потому что не понимают, что именно может сломаться.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;Исправление одной ошибки порождает две новые в других местах.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;Новые члены команды не могут разобраться в коде без длительного погружения и помощи коллег.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;Любая оценка сроков сопровождается большой неопределённостью, потому что никто не знает, с какими скрытыми зависимостями придётся столкнуться.&lt;/li&gt;
&lt;/ul&gt;

&lt;p style="text-align: justify;"&gt;Если хотя бы два из этих признаков присутствуют в вашем проекте -&amp;nbsp;технический долг уже влияет на производительность команды, даже если это ещё не очевидно в цифрах.&lt;/p&gt;

&lt;h2 style="text-align: justify;"&gt;Две составляющие любого программного продукта&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;У каждой программной системы есть два измерения. Трудности начинаются тогда, когда о каком-то из них забывают.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Поведение&amp;nbsp;&lt;/strong&gt;-&amp;nbsp;это то, что система делает: считает, отображает, сохраняет, отправляет. Именно это видит бизнес и чувствуют пользователи. Многие считают, что если код работает -&amp;nbsp;задача решена. Однако это лишь первая половина дела.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Структура&lt;/strong&gt;&amp;nbsp;- это то, насколько легко систему можно изменить.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Само английское слово &lt;em&gt;&lt;strong&gt;software&lt;/strong&gt;&lt;/em&gt;&amp;nbsp;содержит в себе это значение: &lt;em&gt;&lt;strong&gt;soft&lt;/strong&gt;&lt;/em&gt;&amp;nbsp;-&amp;nbsp;мягкий, податливый. Программное обеспечение изначально задумано как то, что легко поддаётся изменениям. Сложность добавления новой возможности должна определяться только масштабом самой этой возможности -&amp;nbsp;но никак не состоянием кодовой базы.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Наглядный пример: &lt;/strong&gt;система формирует отчёты в формате PDF. Бизнес просит добавить выгрузку в Excel. Если архитектура продумана -&amp;nbsp;это задача на несколько дней. Если логика формирования отчёта жёстко связана с логикой генерации PDF -&amp;nbsp;команда переписывает модуль несколько недель.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Что важнее - поведение или структура? Ответ неочевиден, но принципиален.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p style="text-align: justify;"&gt;Система, которая работает безупречно, но которую невозможно изменить, потеряет актуальность при первом же серьёзном изменении требований. Система, которая работает с некоторыми недочётами, но легко поддаётся изменениям, может развиваться и приносить пользу годами. Первую не спасёт даже идеальный функционал -&amp;nbsp;вторую вытянет сама гибкость.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 style="text-align: justify;"&gt;Связанность и согласованность: два понятия, которые стоит знать&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Если говорить о структуре системы чуть более конкретно, то в основе большинства архитектурных решений лежат два фундаментальных понятия: связанность и согласованность. Понимание этих концепций помогает оценивать качество архитектуры не интуитивно, а осознанно.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Связанность&lt;/strong&gt;&amp;nbsp;-&amp;nbsp;это степень зависимости одних частей системы от других. Чем выше связанность, тем сильнее изменение в одном модуле влечёт за собой изменения в других. Высокая связанность -&amp;nbsp;это именно то, что превращает простую задачу в многодневную работу: чтобы поправить одно место, приходится разбираться с десятком других.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Хорошо спроектированная система стремится к низкой связанности. Модули взаимодействуют друг с другом через чётко определённые интерфейсы и не «знают» о внутреннем устройстве соседних компонентов. Это позволяет изменять, тестировать и заменять отдельные части системы независимо друг от друга.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;strong&gt;Согласованность&amp;nbsp;&lt;/strong&gt;-&amp;nbsp;это степень того, насколько элементы одного модуля относятся к одной задаче. Высокая согласованность означает, что каждый модуль делает что-то одно, но делает это хорошо. Низкая согласованность - признак того, что в одном месте собрано слишком много разнородной логики, которую со временем становится всё труднее понимать и поддерживать.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p style="text-align: justify;"&gt;Идеальное сочетание для здоровой архитектуры -&amp;nbsp;низкая связанность и высокая согласованность. Именно к этому балансу стремятся большинство архитектурных принципов и паттернов, о которых мы будем говорить в следующих материалах.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 style="text-align: justify;"&gt;Матрица Эйзенхауэра: почему архитектура всегда проигрывает в приоритетах&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Президент Дуайт Эйзенхауэр сформулировал это так: «У меня есть два вида дел -&amp;nbsp;срочные и важные. Срочные, как правило, не самые важные, а важные -&amp;nbsp;не самые срочные».&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;В разработке эта закономерность проявляется особенно отчётливо.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Новые функции, исправление ошибок, очередной релиз - всё это срочно. Бизнес давит, клиенты ждут. Но это не означает, что именно это наиболее важно для долгосрочной жизнеспособности продукта.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Проектирование, рефакторинг, работа над структурой системы -&amp;nbsp;это важно. Критически важно. Однако никто не приходит к разработчику с задачей «срочно улучши архитектуру». Поэтому она откладывается снова и снова -&amp;nbsp;и это не лень, а системная ошибка в расстановке приоритетов, которую необходимо осознанно исправлять.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p style="text-align: justify;"&gt;Вывод здесь прост: если команда никогда не выделяет время на важное, потому что всегда находится что-то срочное -&amp;nbsp;рано или поздно срочным становится всё.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p style="text-align: justify;"&gt;Система деградирует до состояния, в котором каждое изменение требует экстренного вмешательства, а разработка превращается в постоянную борьбу с последствиями прошлых решений. Именно поэтому работа над архитектурой должна планироваться намеренно и регулярно -&amp;nbsp;не в ущерб срочным задачам, но и не в вечном ожидании подходящего момента, который так и не наступит.&lt;/p&gt;

&lt;h2 style="text-align: justify;"&gt;Как архитектура влияет на команду: человеческое измерение&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Разговор об архитектуре нередко сводится к техническим и экономическим аргументам. Однако есть ещё одно измерение, которое упускают из виду -&amp;nbsp;человеческое.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Работа в условиях плохо спроектированной системы изматывает. Разработчик, который каждый день вынужден продираться сквозь запутанный код, тратит значительную часть своих когнитивных ресурсов не на решение задачи, а на то, чтобы просто понять, что вообще происходит. Это явление называется когнитивной нагрузкой, и оно напрямую влияет на производительность и качество принимаемых решений.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Помимо этого, плохая архитектура негативно сказывается на моральном состоянии команды. Трудно сохранять профессиональную мотивацию, когда понимаешь, что твоя работа -&amp;nbsp;это не создание чего-то нового, а бесконечное латание прорех. Опытные специалисты в таких условиях нередко принимают решение сменить место работы, унося с собой накопленные знания о системе. Это создаёт дополнительные риски для проекта, которые крайне сложно измерить, но очень болезненно ощутить.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p style="text-align: justify;"&gt;Хорошая архитектура, напротив, создаёт среду, в которой разработчики могут сосредоточиться на содержательной работе.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p style="text-align: justify;"&gt;Когда система организована логично, новый член команды способен разобраться в ней значительно быстрее. Когда модули изолированы и независимы, над разными частями продукта можно работать параллельно, не мешая друг другу. Всё это -&amp;nbsp;не абстрактные преимущества, а вполне конкретные факторы, влияющие на скорость и качество разработки.&lt;/p&gt;

&lt;h2 style="text-align: justify;"&gt;Как обосновывать важность архитектуры в диалоге с руководством&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Разработчик -&amp;nbsp;это не просто исполнитель, реализующий поставленные задачи. Это профессионал со своей зоной ответственности. И защита качества системы -&amp;nbsp;неотъемлемая часть этой ответственности.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Менеджеры, маркетологи, отдел продаж будут закономерно требовать скорости -&amp;nbsp;это их работа. Задача разработчика -&amp;nbsp;не противостоять этому давлению молчанием или раздражением, а уметь выстраивать аргументированный диалог.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Несколько практических принципов:&lt;/p&gt;

&lt;ul&gt;
	&lt;li style="text-align: justify;"&gt;&lt;strong&gt;Избегайте субъективных формулировок&lt;/strong&gt;: «код стал некрасивым» или «это технически неправильно» не убеждают никого, кто не занимается разработкой.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;&lt;strong&gt;Говорите на языке конкретных последствий&lt;/strong&gt;: «Если мы выделим три дня на оптимизацию этого модуля сейчас, следующий этап займёт неделю, а не месяц».&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;&lt;strong&gt;Фиксируйте технический долг явно.&lt;/strong&gt; Если ваша команда принимает осознанное решение сделать что-то упрощённо ради скорости -&amp;nbsp;запишите это. Создайте задачу, опишите, что именно было сделано не так, как следовало бы, и почему. Это переводит технический долг из категории неосознанного накопления в категорию управляемого инструмента. Руководство видит, что команда отдаёт себе отчёт в принятых решениях, а не просто пишет «как получается».&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;&lt;strong&gt;Внедрите принцип постепенного улучшения&lt;/strong&gt;: каждый раз, работая с определённым участком кода, оставляйте его в немного лучшем состоянии, чем он был до вашего вмешательства. Это не требует отдельного времени -&amp;nbsp;это профессиональная привычка, которая со временем существенно меняет общее состояние системы.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;&lt;strong&gt;Предлагайте компромиссы, а не ультиматумы. &lt;/strong&gt;Вместо «нам нужна неделя на рефакторинг, иначе ничего не выйдет» попробуйте «давайте заложим в план двадцать процентов времени на улучшение структуры параллельно с основной работой». Это звучит управляемо и вызывает значительно меньше сопротивления.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 style="text-align: justify;"&gt;Заключение&lt;/h2&gt;

&lt;p style="text-align: justify;"&gt;Если сформулировать главное из этого материала, получится следующее.&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
	&lt;li style="text-align: justify;"&gt;Качество архитектуры измеряется не технической изощрённостью решений, а тем, насколько легко и предсказуемо в систему вносятся изменения спустя месяцы и годы после запуска.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;Поддерживать порядок в коде с самого начала выгоднее, чем устранять последствия накопленного беспорядка -&amp;nbsp;это не вопрос эстетики, а вопрос экономики разработки.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;Технический долг -&amp;nbsp;управляемый инструмент, когда он осознан и зафиксирован. Неуправляемая угроза -&amp;nbsp;когда накапливается незаметно и без плана по погашению.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;Гибкость системы в долгосрочной перспективе важнее идеального функционала в моменте. Рынок меняется, требования меняются -&amp;nbsp;продукт, который не способен меняться вместе с ними, неизбежно теряет ценность.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;Архитектура влияет не только на код, но и на людей. Среда, в которой разработчики могут работать уверенно и без лишних препятствий -&amp;nbsp;это конкурентное преимущество, которое сложно измерить, но легко потерять.&lt;/li&gt;
	&lt;li style="text-align: justify;"&gt;И наконец: умение объяснить всё это руководству на понятном бизнесу языке -&amp;nbsp;такой же профессиональный навык, как и умение проектировать надёжные системы.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p style="text-align: justify;"&gt;&lt;em&gt;&lt;strong&gt;Спасибо, что прочитали материал до конца.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;Данная статья является вводной и представляет собой лишь первый шаг в изучении архитектуры программного обеспечения. Тема значительно глубже, чем можно охватить в одном материале, и впереди ещё много интересного.&lt;/p&gt;

&lt;p style="text-align: justify;"&gt;&lt;em&gt;&lt;strong&gt;Следите за продолжением.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;</summary>
    <dc:creator>Romo Fedoroff</dc:creator>
    <dc:date>2026-05-01T12:54:00Z</dc:date>
  </entry>
  <entry>
    <title>Анатомия письма: почему email-дизайн — это машина времени в 1999 год</title>
    <link rel="alternate" href="https://www.tune-it.ru/en/c/blogs/find_entry?entryId=21239713" />
    <author>
      <name>Алексей Кондратьев</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21239144" />
    <author>
      <name>Maxim Kalabukhov</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21238639" />
    <author>
      <name>Никита Рогаленко</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21236519" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21235647" />
    <author>
      <name>Andrei Maksimov</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21227823" />
    <author>
      <name>Алексей Кондратьев</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21224493" />
    <author>
      <name>Maxim Kalabukhov</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21224460" />
    <author>
      <name>Polina Napolskaya</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21222577" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21221410" />
    <author>
      <name>Никита Рогаленко</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21216208" />
    <author>
      <name>Maxim Kalabukhov</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21213820" />
    <author>
      <name>Polina Napolskaya</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21212798" />
    <author>
      <name>Алексей Кондратьев</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21212001" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21211280" />
    <author>
      <name>Никита Рогаленко</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21207336" />
    <author>
      <name>Vadim Mikhu</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21203995" />
    <author>
      <name>Polina Napolskaya</name>
    </author>
    <id>https://www.tune-it.ru/en/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/en/c/blogs/find_entry?entryId=21202234" />
    <author>
      <name>Romo Fedoroff</name>
    </author>
    <id>https://www.tune-it.ru/en/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>
</feed>

