<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 14:58:37 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="5-DAXcsPiCr0nl0FYEEQ3kTss_cwKdBjWwLvg-rvzRwIMQtqOXElSkLdeZ1sTuCphOWeXTgeLsHm4nXXuOgqcg";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>Архитектура | JS: Предметно-ориентированное проектирование</title>
<meta name="description" content="Архитектура / JS: Предметно-ориентированное проектирование: Знакомимся с несколькими важными аспектами архитектур и рассмотреть стандартные, принятые в индустрии подходы к проектированию систем">
<link rel="canonical" href="https://ru.hexlet.io/courses/js-ddd/lessons/architecture/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Архитектура">
<meta property="og:title" content="JS: Предметно-ориентированное проектирование">
<meta property="og:description" content="Архитектура / JS: Предметно-ориентированное проектирование: Знакомимся с несколькими важными аспектами архитектур и рассмотреть стандартные, принятые в индустрии подходы к проектированию систем">
<meta property="og:url" content="https://ru.hexlet.io/courses/js-ddd/lessons/architecture/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="hPrEsd1VH1uho_7n8-REQzr5YxGM0voGx4qJxvADLKVrKw-GLyuyOxfg2n__67Q0-vBOu4TlBKR6ahOSogTLyw" />
<script src="/vite/assets/inertia-INZxX8jp.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-6pOtQ3OW.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAwNywicHVyIjoiYmxvYl9pZCJ9fQ==--f0b38f0e25ed59255acec6eaeaeec0a99aec453f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Binary%20code-rafiki.png"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T14:58:36.866Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"22rV-VxYQVLtcrNi3xEezkpEFH6DbKyAGkvZtfG1DSg0ux7OribsMlsxl_rTHu65ik051ItbUiKnq0Pho7LqRg","topics":[{"id":8860,"title":"Здравствуйте, картинки этого и предыдущих уроков(да и курсов) сломаны:\n> <img src=\"\" alt=\"architecture\">","plain_title":"Здравствуйте, картинки этого и предыдущих уроков(да и курсов) сломаны: ","creator":{"public_name":"Denis Eremin","id":140068,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":17677,"body":"Тест","topic_id":8860},{"creator":{"public_name":"R. D.","id":3,"is_tutor":false},"id":17659,"body":"Поправил, спасибо!","topic_id":8860}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":44483,"title":"Не могу понять, почему в задаче мы возвращаем сущности из сервисов? В той же теории говорится, что так делать не надо...","plain_title":"Не могу понять, почему в задаче мы возвращаем сущности из сервисов? В той же теории говорится, что так делать не надо... ","creator":{"public_name":"Сергей Шиховцов","id":175587,"is_tutor":false},"comments":[{"creator":{"public_name":"Sergei Melodyn","id":162475,"is_tutor":true},"id":96277,"body":"**Сергей Шиховцов**, приветствую.\n\nОбратите внимание на эту строку в теории:\n\n> чтобы не усложнять, в тех системах, где нет готовых механизмов для трансляции, возвращают и сущности\n\nЭто как раз наш кейс, что в рамках упражнения проще обойтись без DTO.","topic_id":44483}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":30336,"title":"увидел метод all, не понял зачем он, нашел об этом в топиках и ответ:\n>all удалил)\n\nА он все равно в решении учителя есть","plain_title":"увидел метод all, не понял зачем он, нашел об этом в топиках и ответ: all удалил) А он все равно в решении учителя есть ","creator":{"public_name":"Сергей Егупов","id":184023,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":65740,"body":"Такс, теперь должно быть ок.","topic_id":30336},{"creator":{"public_name":"Сергей Егупов","id":184023,"is_tutor":false},"id":65741,"body":"Хм. У меня пока на месте и в ревью и при просмотре решения в упражнении. ","topic_id":30336},{"creator":{"public_name":"Сергей Егупов","id":184023,"is_tutor":false},"id":65744,"body":"Да. Точно. Все ок)","topic_id":30336},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":65743,"body":"В ревью оно никуда не денется), там зафиксировано то что было на момент ревью. Чтобы увидеть новое упражнение, надо сбросить прогресс.","topic_id":30336}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":27869,"title":"> И последнее по списку, но не последнее по важности, не вызывайте сервисы из сервисов. \n\nМожете привести пример нарушения данной рекомендации и возможные последствия?","plain_title":"И последнее по списку, но не последнее по важности, не вызывайте сервисы из сервисов. Можете привести пример нарушения данной рекомендации и возможные последствия? ","creator":{"public_name":"","id":473,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":59868,"body":"При таком подходе сервисы начнуть использоваться на одном уровне с моделями, так как модели вызываются на уровне сервисов. А это значит что слой сервисов не покрывает модели, а находится с ним на одном уровне. В такой ситуации теряется весь смысл создания покрывающего слоя.","topic_id":27869}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":51431,"title":"Помогите разобраться, пожалуйста. В чем ошибка?\nhttps://ru.hexlet.io/code_reviews/365155","plain_title":"Помогите разобраться, пожалуйста. В чем ошибка? https://ru.hexlet.io/code_reviews/365155 ","creator":{"public_name":"Татьяна Пантелейчук","id":162776,"is_tutor":false},"comments":[{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":109949,"body":"Татьяна, приветствую!\n\nВ методе createCinemaHall() все имена `сinemaHall` и `СinemaHallRepository` содержат первую русскую букву `С` (`с`). Визуально не отличимо, но для интерпретатора это уже совершенно другие имена.","topic_id":51431},{"creator":{"public_name":"Татьяна Пантелейчук","id":162776,"is_tutor":false},"id":109952,"body":"**Станислав Дзисяк**, спасибо! не уследила..","topic_id":51431}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":49927,"title":"Сейчас на проекте немного другой подход : контроллер формирует ДТО и вызывает необходимый сервис, сервис выполняет свою логику и вызывает репозиторий куда пробрасывает ДТО, а репозиторий создаёт сущность и сохраняет или как-то еще взаимодействует с сущностями в БД. Подскажите чем плох такой подход, почему сущность должна создаваться в сервисе? (с практической точки зрения) ","plain_title":"Сейчас на проекте немного другой подход : контроллер формирует ДТО и вызывает необходимый сервис, сервис выполняет свою логику и вызывает репозиторий куда пробрасывает ДТО, а репозиторий создаёт сущность и сохраняет или как-то еще взаимодействует с сущностями в БД. Подскажите чем плох такой подход, почему сущность должна создаваться в сервисе? (с практической точки зрения) ","creator":{"public_name":"Gennadii Khotovytskyi","id":226560,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":107178,"body":"Не вижу тут ничего плохого. Скажу так, все это зависит от конкретной экосистемы. Когда речь идет про ДТО, то скорее всего мы говорим о Java/C#. В динамических языках так практически никогда не делают, потому что можно просто сформировать объект/хеш/мапу и прокинуть дальше.\n\n","topic_id":49927}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":7292,"title":"Про вызов сервисов из сервисов хочется понять подробнее. \nИнтуитивно это кажется нормальной практикой да и встречается постоянно. \n\nМожет быть дело в том, что термин \"сервис\" перегружен, и нужно больше контекста.\nНагуглил обсуждение [softwareengineering.stackexchange.com/questions/218011/](http://softwareengineering.stackexchange.com/questions/218011/how-accurate-is-business-logic-should-be-in-a-service-not-in-a-model) В верхнем ответе есть определение понятия сервис, речь примерно про это?","plain_title":"Про вызов сервисов из сервисов хочется понять подробнее. Интуитивно это кажется нормальной практикой да и встречается постоянно. Может быть дело в том, что термин \"сервис\" перегружен, и нужно больше контекста. Нагуглил обсуждение softwareengineering.stackexchange.com/questions/218011/ (http://softwareengineering.stackexchange.com/questions/218011/how-accurate-is-business-logic-should-be-in-a-service-not-in-a-model) В верхнем ответе есть определение понятия сервис, речь примерно про это? ","creator":{"public_name":"Alexander Moskvichev","id":42943,"is_tutor":false},"comments":[{"creator":{"public_name":"Юрий Быков","id":36683,"is_tutor":false},"id":13815,"body":"Сервисы бывают разные: доменные, приложения, инфраструктуры. Часть логики, которую нельзя упаковать в какую-либо сущность домена, упаковываем в сервис домена и его уже вызываем в нашем use case (сервис приложения).\n\nПример: сущность создаётся внутри другой сущности, но при этом первая сущность принимает в конструктор некий параметр из общего справочника. Получение данных из справочника это уже инфраструктура, её внутрь домена никак не поставить, иначе нарушится persistence ignorance и SRP.\n\n```\nconst defaultDirection = 'OUTPUT';\n\nclass Func {\n id,\n parameters,\n addParameterByName(name, direction = null) {\n # в чистом домене так делать нельзя dataProvider.get()\n direction = direction || dataProvider.get(defaultDirection);\n this.parameters.push(new Parameter(this, name, direction));\n }\n}\n\nclass Parameter {\n id,\n name,\n direction,\n func,\n constructor(func, name, dir) {\n this.func = func;\n this.name = name;\n this.dir = dir;\n }\n}\n\nconst func = new Func();\nfunc.addParameterByName('flow_best_parameter');\n```\n\nИз практики 3 урока: код конструктора, содержащий вызов инфраструктуры.\n```\nthis.id = uuid.create().hex;\n```\n\nВозможно, получилось избыточно, но можно потратить кучу времени, чтобы найти правильный путь.","topic_id":7292},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":13542,"body":"Сервис как слой находится поверх доменной модели. Из этого следует, что сервис нельзя использовать на том же уровне, на котором используются сущности. Как только кто-то начинает вызывать сервис внутри метода сервиса, то почти наверняка там будет оперирование сущностями и даже если не будет сразу, то потом оно появится легче легкого. Нарушатся барьеры абстракции.\n\nГораздо более лучшей стратегией будет общий код для сервисов выделять во внутренние блоки, которые можно переиспользовать.","topic_id":7292}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":60965,"title":"Не совсем понятно как определять какая логика должна быть в доменном слое а какая в сервисном.\nВ пройденном ранее уроке \"Сущности и связи\", добавление объекта **FilmScreening** в список **filmScreenings** объекта **CinemaHall** происходит в конструкторе **FilmScreening**\n```\nexport default class FilmScreening {\n constructor(film, cinemaHall, time) {\n this.id = uuid.create().toString();\n this.film = film;\n this.cinemaHall = cinemaHall;\n this.time = time;\n this.createdAt = new Date();\n\n this.cinemaHall.addFilmScreening(this);\n }\n}\n```\nно это можно сделать и в сервисе. Например:\n```\nexport default class extends ApplicationService {\n createFilmScreening(filmId, cinemaHallId, time) {\n const film = this.FilmRepository.find(filmId);\n const hall = this.CinemaHallRepository.find(cinemaHallId);\n const filmScreening = new FilmScreening(film, hall, time);\n // добавление показа фильма в зал\n hall.addFilmScreening(filmScreening);\n ...\n }\n}\n```\nпочему это нельзя делать в сервисе в данном случае?\nПока не уловил четкую границу между сервисным слоем и доменным.","plain_title":"Не совсем понятно как определять какая логика должна быть в доменном слое а какая в сервисном. В пройденном ранее уроке \"Сущности и связи\", добавление объекта FilmScreening в список filmScreenings объекта CinemaHall происходит в конструкторе FilmScreening ``` export default class FilmScreening { constructor(film, cinemaHall, time) { this.id = uuid.create().toString(); this.film = film; this.cinemaHall = cinemaHall; this.time = time; this.createdAt = new Date(); this.cinemaHall.addFilmScreening(this); } } но это можно сделать и в сервисе. Например: export default class extends ApplicationService { createFilmScreening(filmId, cinemaHallId, time) { const film = this.FilmRepository.find(filmId); const hall = this.CinemaHallRepository.find(cinemaHallId); const filmScreening = new FilmScreening(film, hall, time); // добавление показа фильма в зал hall.addFilmScreening(filmScreening); ... } } ``` почему это нельзя делать в сервисе в данном случае? Пока не уловил четкую границу между сервисным слоем и доменным. ","creator":{"public_name":"Factory Factory","id":184938,"is_tutor":false},"comments":[{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":128679,"body":"**Factory Factory**, приветствую!\n\nВ предыдущем уроке рассматривалась работа только со слоем сущностей, но в текущем были добавлены репозитории и сервисы. Соответственно часть логики которая ранее находилась внутри доменного слоя, теперь разнесена по репозиториям и сервисам. Направление вашей мысли в целом верное, сервис отвечает за определённый сценарий, в данном случае создание сущности FilmScreening. Этот сценарий подразумевает получение film, hall, создание инстанса filmScreening и сохранение его в репозитории. Порекомендую вам пройти упражнение, а после изучить и разобрать решение учителя. ","topic_id":60965}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":10094,"title":"> Ни в коем случае не вызывайте сервисы из сервисов. Если появляется общий код, то выносите общую функциональность, но не позволяйте самому сервису начинать мешаться с доменом.\n\nНе могли бы вы привести пример. Не понятно почему нельзя вызывать сервис из сервиса, и при чем тут общая функциональность? и почему это называется мешать сервис с доменом?","plain_title":"Ни в коем случае не вызывайте сервисы из сервисов. Если появляется общий код, то выносите общую функциональность, но не позволяйте самому сервису начинать мешаться с доменом. Не могли бы вы привести пример. Не понятно почему нельзя вызывать сервис из сервиса, и при чем тут общая функциональность? и почему это называется мешать сервис с доменом? ","creator":{"public_name":"Андрей Соколов","id":88518,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":20783,"body":"Потому что если у вас в рамках одного кода встречается сервисная функция и работа с сущностями, то это нарушение границ абстракций (функция сервиса это законченная бизнес операция, которая инкапсулирует работу с доменом внутри себя). Поэтому правильный подход заключается в том чтобы не переиспользовать сервисные функции, а выносить общий код на уровень ниже.","topic_id":10094}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}},{"id":10330,"title":"на 9 шаге (первичная реализация BaseRepository и CinemaService) в реализации BaseRepository у учителя свойство data представлено как статическое свойство класса (если я правильно понял) и как массив. Я же в своем решении реализовал это свойство хранилища в конструкторе как this.storage = {}. Т.е. как свойство конкретного репозитория и через хеш таблицу (что кажется логичней и эффективней). Кажется что мой подход более правильный и больше соответсвует заданию. Поясните пожалуйста какой подход правильнее, и почему это реализовано именно так у учителя? Или может быть это не так критично?","plain_title":"на 9 шаге (первичная реализация BaseRepository и CinemaService) в реализации BaseRepository у учителя свойство data представлено как статическое свойство класса (если я правильно понял) и как массив. Я же в своем решении реализовал это свойство хранилища в конструкторе как this.storage = {}. Т.е. как свойство конкретного репозитория и через хеш таблицу (что кажется логичней и эффективней). Кажется что мой подход более правильный и больше соответсвует заданию. Поясните пожалуйста какой подход правильнее, и почему это реализовано именно так у учителя? Или может быть это не так критично? ","creator":{"public_name":"Андрей Соколов","id":88518,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":21344,"body":"> у учителя свойство data представлено как статическое свойство класса\n\nЕсли бы оно было статическое, то там было бы слово `static`, но его там нет.\n\n> Я же в своем решении реализовал это свойство хранилища в конструкторе как this.storage = {}\n\nМожно в конструкторе, а можно и так как в моем решении потому что это одно и тоже.","topic_id":10330}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Архитектура","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":586,"slug":"js_ddd_architecture_exercise","name":null,"state":"active","kind":"exercise","language":"javascript","locale":"ru","has_web_view":false,"has_test_view":false,"reviewable":true,"readme":"### lib/BaseRepository.js\n\nРеализуйте базовый класс репозиторий со следующими методами:\n\n* `save(entity)`\n* `find(id)`\n\nПринцип работы:\n\n```javascript\nconst repository = new CinemaHallRepository()\nrepository.save(cinemaHall)\nconst hall = repository.find(cinemaHall.id)\n```\n\n### services/CinemaService.js\n\nКаждая реализация бизнес-сценария в сервисе самостоятельно отвечает за сохранение\nсозданных сущностей в соответствующих репозиториях. Как пример, можно посмотреть\nреализацию `createFilm`. Доступ к репозиториям осуществляется через `this`.\n\nРеализуйте следующие методы (бизнес-сценарии):\n\n#### Добавление кинозала\n\n```javascript\nconst cinemaHall = service.createCinemaHall(name, rows, cols)\n```\n\n#### Добавление сеанса\n\n```javascript\nconst filmScreening = service.createFilmScreening(filmId, cinemaHallId, time)\n```\n","prepared_readme":"### lib/BaseRepository.js\n\nРеализуйте базовый класс репозиторий со следующими методами:\n\n* `save(entity)`\n* `find(id)`\n\nПринцип работы:\n\n```javascript\nconst repository = new CinemaHallRepository()\nrepository.save(cinemaHall)\nconst hall = repository.find(cinemaHall.id)\n```\n\n### services/CinemaService.js\n\nКаждая реализация бизнес-сценария в сервисе самостоятельно отвечает за сохранение\nсозданных сущностей в соответствующих репозиториях. Как пример, можно посмотреть\nреализацию `createFilm`. Доступ к репозиториям осуществляется через `this`.\n\nРеализуйте следующие методы (бизнес-сценарии):\n\n#### Добавление кинозала\n\n```javascript\nconst cinemaHall = service.createCinemaHall(name, rows, cols)\n```\n\n#### Добавление сеанса\n\n```javascript\nconst filmScreening = service.createFilmScreening(filmId, cinemaHallId, time)\n```\n","has_solution":true,"entity_name":"Архитектура"},"units":[{"id":1725,"name":"theory","url":"/courses/js-ddd/lessons/architecture/theory_unit"},{"id":1762,"name":"quiz","url":"/courses/js-ddd/lessons/architecture/quiz_unit"},{"id":1733,"name":"exercise","url":"/courses/js-ddd/lessons/architecture/exercise_unit"}],"links":[],"ordered_units":[{"id":1725,"name":"theory","url":"/courses/js-ddd/lessons/architecture/theory_unit"},{"id":1762,"name":"quiz","url":"/courses/js-ddd/lessons/architecture/quiz_unit"},{"id":1733,"name":"exercise","url":"/courses/js-ddd/lessons/architecture/exercise_unit"}],"id":846,"slug":"architecture","state":"approved","name":"Архитектура","course_order":25,"goal":"Знакомимся с несколькими важными аспектами архитектур и рассмотреть стандартные, принятые в индустрии подходы к проектированию систем","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"При разработке приложений с богатой предметной областью во весь рост встаёт вопрос о том, как правильно организовать код приложения, а если смотреть шире, то какую выбрать архитектуру. О том, какие варианты может предложить вам индустрия мы сейчас и поговорим, но перед тем, как я расскажу про существующие подходы, важно запомнить несколько вещей.\n\nНе существует единственного верного подхода при организации вашего приложения. Известные подходы — всего лишь видение конкретных людей для конкретных ситуаций и конкретных стеков (язык + инструментарий). Любая хорошая архитектура базируется на фундаментальных законах и принципах. Большую часть из них вы уже знаете:\n\n* Изоляция побочных эффектов;\n* Хорошая абстракция (абстракция данных, композиция, разделение);\n* Сильные барьеры (между абстракциями);\n* Слабые связи (возможность замены/независимого развития).\n\nИз популярного можно выделить следующие словосочетания:\n\n* The Clean Architecture\n* Onion Architecture\n* Hexagonal Architecture\n\nВсе эти архитектуры сводятся так или иначе к тому, что наше приложение представляет из себя набор слоёв (тех самых абстракций), которые связаны друг с другом определённым образом и отвечают за определённые аспекты системы. Лучше всего начать анализ с красивой картинки:\n\n\n\nНачать стоит с того, что: _Фреймворк — это не ваше приложение_. Остановитесь на секундочку и хорошо обдумайте фразу. В типичных веб-приложениях фреймворк определяет вообще всё. Приложение на 100% переплетается с ним и становится его частью. Программист начинает мыслить в рамках возможностей фреймворка и его ограничений, и в его голове появляются несуществующие причинно-следственные связи.\n\nДа, конечно, сложно (и не нужно) делать абсолютную изоляцию, но и всегда нужно проводить чёткую грань между приложением и тем фреймворком, который вы используете.\n\n## Домен\n\nПервым и базовым слоем в приложении является Домен. Это реализация вашей модели предметной области. Чистая бизнес-логика без намёка на инфраструктуру.\n\nВот что обычно характеризует домен:\n\n* Чистый код (pure)\n* Plain Old X Object (POXO)\n* Бизнес-логика\n* Валидация\n\n`POXO` - это обобщённое название, которое в каждом конкретном языке приобретает своё собственное имя. В Java `POJO`, в Ruby `PORO`, и так далее. Этой аббревиатурой описывают объекты, которые построены исключительно на возможностях самого языка, без дополнительных абстракций. Так подчёркивается, что домен не использует внешних библиотек, которые влияют на его организацию. Не надо фанатично относиться к этой идее. В некоторых языках сформировались свои правила, и они идут вразрез с общими концепциями.\n\n## Персистентность\n\nРеализовать логику только половина дела. В конце концов нужно сохранить наши изменения. Казалось бы, что эта часть должна быть самой простой, но нет. Состояние, его изменение и поддержка целостности настолько сложная история, что придуманы огромные и сложные фреймворки, называемые `ORM`. Обычно они построены вокруг двух самых распространённых паттернов:\n\n* ActiveRecord\n* DataMapper\n\nВ этом курсе мы их не рассматриваем, но в реальной жизни вам придётся с ними столкнуться.\n\n## Репозиторий\n\nРепозиторий — это хранилище однотипных сущностей. Позволяет как делать выборки, так и сохранять сущности внутри себя. Для простоты в нашем приложении репозитории будут хранить все данные в памяти.\n\n```javascript\nconst film = new Film(name, duration)\nrepository = new FilmRepository()\nrepository.save(film)\n\nrepository.find(film.id) // film\nrepository.find(unknownId) // Boom!\n```\n\n## Инфраструктура\n\nИменно в эту категорию попадает фреймворк, UI и вообще любая прикладная история. На картинке этот слой находится на самой внешней стороне. Из него происходит отправка электронных писем, смс, и выполняется так называемая логика приложения. Например, перенаправление на определённую страницу после создания какой-то сущности.\n\n## Сервисы\n\nПомните диаграмму вариантов использования? Вот именно эти варианты и являются единственным способом изменения состояния вашего приложения. Ваш домен обрастает слоем так называемых сервисов. Каждый сервис представляет собой набор функций, имитирующих бизнес-сценарии, например, \"добавить в друзья\", \"поставить лайк\".\n\n```javascript\nclass CinemaService {\n createFilm(name, duration) {\n const film = new Film(name, duration)\n this.FilmRepository.save(film)\n return film\n }\n}\n```\n\nИнфраструктурный слой является главным пользователем вашего слоя сервисов. Сервисы могут вызываться в `ui`, в контроллерах, в асинхронных обработчиках. Слой сервисов настолько важен сам по себе, что Мартин Фаулер описывает его как шаблон проектирования [Service Layer](https://martinfowler.com/eaaCatalog/serviceLayer.html)\n\nПри проектировании сервисов нужно придерживаться некоторых правил, которые позволяют абстракции не протекать и максимально поддерживать чистоту.\n\nВходными данными в функции сервиса не могут быть сущности предметной области. Причина такого правила очень проста. Сервисы – слой поверх предметной области, он инкапсулирует в себе все сценарии. Если сущности окажутся снаружи, то логика становится размазанной между слоями (потекла абстракция), пропадает изоляция. Но так сделать не всегда возможно. Иногда это связано с устройством конкретных фреймворков, которые не дают нормально абстрагировать предметную область от инфраструктуры. В такой ситуации не стоит бороться насмерть за концептуальную чистоту, идите на компромиссы.\n\nТо же самое касается выходных данных. В теории, отдавать наружу сущности нельзя по той же причине, по которой нельзя ими оперировать вне сервисов. Так как после возврата крайне просто начать ей оперировать, что сразу повлечёт за собой размазывание логики по слоям. Вместо сущности, как правило, отдают специальный \"Data Transfer Object\". В отличие от сущности он не содержит поведения и используется исключительно как контейнер для чтения.\n\n> DTO - Используется для передачи данных между подсистемами приложения. DTO, в отличие от business object, не должен содержать какого-либо поведения\n\n* Неизменяемый\n* Просто данные\n\nОпять же, чтобы не усложнять, в тех системах, где нет готовых механизмов для трансляции, возвращают и сущности, но на уровне соглашений используют их исключительно как `DTO`.\n\nИ последнее по списку, но не последнее по важности, не вызывайте сервисы из сервисов. Если появляется общий код, то выносите общую функциональность, но не позволяйте самому сервису начинать мешаться с доменом. Последнее означает то, что если сервисы начинают использовать внутри себя сервисы (тот же или другие), то с большой вероятностью происходит нарушение принципа одного уровня абстракции. Сервисы — слой _поверх_ домена, а это значит, что на одном уровне нельзя использовать и домен, и сервис.\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":1727,"name":"theory","url":"/courses/js-ddd/lessons/intro/theory_unit"}],"links":[],"ordered_units":[{"id":1727,"name":"theory","url":"/courses/js-ddd/lessons/intro/theory_unit"}],"id":848,"slug":"intro","state":"approved","name":"Введение","course_order":10,"goal":"Знакомимся с целями и задачами курса","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Обычно во вступлении мы рассказываем то, что ожидает вас внутри курса, но здесь я решил\nрассказать кое-что важное. Попробуйте самостоятельно ответить на вопрос. Какая основная\nзадача программиста?\n\nВероятно, вы ответите \"писать код\" и будете не правы. Писать код это всего лишь средство,\nпричём не единственное. Также часто решением задачи является удаление кода или, вообще,\nотсутствие кода, и всё это тоже область компетенции программиста.\n\nНачать нужно с того, что программирование, как таковое, это не цель, это всего лишь\nсредство достижения бизнес-целей той компании, которая вас нанимает. В конечном итоге\nвсё программное обеспечение так или иначе служит удовлетворению потребностей бизнеса:\nувеличению прибыли, снижению издержек. [Хорошая статья об этом](https://ru.hexlet.io/blog/posts/developers-business-value) есть в нашем блоге.\n\nНа практике это означает очень простую вещь, перед тем как бросаться писать код,\nнужно понять цель того, что вам нужно сделать. Хочу ещё раз акцентировать ваше внимание, на том,\nчто цель это **зачем** мы это делаем, а не **что** нужно сделать. У меня\nесть хорошая аналогия, которую мы постоянно наблюдаем в своей жизни. Вспомните приходы\nк доктору. Многие люди пытаются рассказывать доктору не только симптомы, но и\nвыдвигают гипотезы, а некоторые прямо утверждают, что у них конкретная болезнь и, более того,\nони знают, как лечиться. Доктора обычно пропускают это мимо ушей, потому что его\nзадача понять истинную причину. То же самое часто происходит в разработке. К вам приходит\nзаказчик и говорит, **что** нужно сделать. Например: \"Вася, добавь две колонки в базу\".\nВозникает парадоксальная ситуация, чем более технически подкован заказчик тем, как правило,\nон больше пытается продавливать конкретные решения, вместо того, чтобы описывать свою\nбизнес-задачу (цель), оставляя вам манёвр для решения.\n\nИзбежать этого невозможно, никто и никогда не будет давать идеальных задач, которые\nсозданы исходя из бизнес-целей. Такое, конечно же, бывает, но гораздо реже, чем вам\nможет показаться. В итоге бизнес-аналитикой занимается разработчик (кроме сложных случаев),\nи это нормально. Докопавшись до сути, может оказаться так, что кода писать не надо вообще\nи достаточно поменять правила игры.\n\nДальше по курсу мы будем исходить из того, что все цели уже определены и нужно именно\nписать код, но перед тем, как мы двинемся дальше, я расскажу о том, как смотреть\nна мир глазами бизнеса и почему это полезно.\n\nПодумайте вот о чём. Откуда бизнес узнает, что нужно делать? Работая на дядю может сложиться\nвпечатление, что там наверху умные люди, которые знают, что делают. На самом деле они не знают.\nПредставьте, что вы начинаете с нуля свой стартап. После непродолжительного анализа станет\nпонятно, что основная сложность не в том, чтобы понять \"что делать\", а в том, чтобы понять\n\"что не делать\". На эту тему есть обязательная книга к прочтению, которая поменяет\nваше мировоззрение: \"Бизнес с нуля. Метод Lean Startup.\"\n\n## Lean Startup\n\n\n\nНе обращайте внимание на слово \"стартап\" в заголовке, эта методология одинаково хорошо работает\nи для больших бизнесов и для молодых проектов. Удивительно, но основная идея этого подхода\nпришла из научного мира и называется \"научный метод\":\n\n```\nНау́чный ме́тод — совокупность основных способов получения новых знаний и методов решения\nзадач в рамках любой науки.\n\nМетод включает в себя способы исследования феноменов, систематизацию, корректировку новых\nи полученных ранее знаний. Умозаключения и выводы делаются с помощью правил и принципов\nрассуждения на основе эмпирических (наблюдаемых и измеряемых) данных об объекте. Базой\nполучения данных являются наблюдения и эксперименты. Для объяснения наблюдаемых фактов\nвыдвигаются гипотезы и строятся теории, на основании которых в свою очередь строится\nматематическое описание — модель изучаемого объекта.\n```\n\nПервое. Логика контринтуитивна. Понять, что нужно вашим пользователям заранее и без общения\nс ними, практически невозможно. Используя lean startup мы выдвигаем гипотезы, а не\nпродумываем конкретные решения. Пример гипотез:\n\n```\nПользователи хотят заказывать такси без необходимости звонить оператору и диктовать адрес.\n\nПользователю удобнее оплачивать такси с карты, чем наличными\n```\n\nПроницательный читатель увидит, что при таком подходе, нет цели реализовать сразу всё, от и\nдо продумав все части программы. Задачей станет реализовать только то, что может помочь\nподтвердить или опровергнуть гипотезу. Ведь если гипотеза не верна, это автоматически означает,\nчто нужно корректировать все дальнейшие планы. В противном случае будут большие потери.\n\nПосле того, как гипотеза готова, делается всё необходимое для её проверки. Многие гипотезы,\nпо факту, не требуют написания кода вообще. Например, гипотеза про удобство оплаты такси\nкартой проверяется звонками друзьям/постами в соцсети. Согласитесь, что это сильно дешевле,\nпроще и быстрее, чем месяцами писать приложение, а потом увидеть, что это никому не нужно.\n\nНа выходе получается цепочка: Гипотеза -> Реализация (если нужно) -> Анализ данных. Повторяя\nэту цепочку снова и снова, мы получаем продукт, который действительно работает и отвечает\nбизнес-целям.\n\nКлючевые слова для самообразования:\n\n* Customer development\n* Business Model Canvas\n* Minimum Viable Product\n* Pivot\n\n## SMART\n\nКогда речь идёт про уже существующий бизнес или, даже, личные цели, то подойдёт такой\nподход как [SMART](https://ru.wikipedia.org/wiki/SMART):\n\n```\nЭто мнемоническая аббревиатура, используемая в менеджменте и проектном управлении для\nопределения целей и постановки задач:\n\n* конкретный (specific);\n* измеримый (measurable);\n* достижимый (attainable);\n* значимый (relevant);\n* соотносимый с конкретным сроком (time-bounded)\n```\n\nЭтот подход хорошо расписан в вики, поэтому не буду заниматься копипастой.\n\n## Impact mapping\n\n\n\nImpact Mapping простая и эффективная техника для определения целей заказчика\nи передача этих целей разработчикам.\n\nImpact Mapping — это диаграмма связей (mind map) по целям проекта с картой влияний, которые должны\nподтолкнуть бизнес заказчика к достижению целей.\n\n### Why?\n\nЦентральный элемент нашей карты, который отвечает на ключевой вопрос:\nЗачем мы это делаем? Это цель, которую бизнес пытается достичь.\n\n### Who?\n\nНа первом уровне мы отвечаем на вопросы: Кто поможет достичь желаемого результата?\nКто может помешать? Кто пользователи нашего продукта? Сюда войдут все заинтересованные стороны, которые могут повлиять на цели бизнеса.\n\n### How?\n\nНа втором уровне мы должны описать воздействия, которые должны оказать заинтересованные\nстороны, чтобы бизнес достиг целей. Мы ищем ответ на вопросы: Как они помогут бизнесу\nдостичь целей? Как они могут помешать успеху проекта?\n\n### What?\n\nПосле ответа на основные вопросы можно обсудить конкретные задачи. Третий уровень\nотвечает на вопросы: Что мы можем сделать как организация или команда разработки,\nчтобы создать необходимые воздействия? Здесь будет описан конечный результат нашей работы.\n\nПодробнее об этом подходе можно прочитать в\n[замечательной статье](https://habrahabr.ru/post/246401/) Александра Бындю на Хабре.\n\n## User Story Mapping\n\n\n\nПосле определения карты влияний на цели можно определить роли пользователей,\nкак они будут взаимодействовать с системой, важность задач, план релизов и т.д.\n\nЦель `user story mapping` в том, чтобы приоритезировать пользовательские истории\nпо важности.\n\nПример пользовательской истории:\n\n```\nЯ, как менеджер по продажам, хочу видеть отчёт по интересам клиентов в курсах, для того,\nчтобы принять решение о создании нового курса и приглашения этих клиентов принять в нём участие.\n```\n\nОб этой полезной технике можно найти много статей на просторах сети. Подробнее\nна ней останавливаться не будем, пора переходить к самому курсу).\n\n\n## Проект: Электронная продажа билетов\n\n\n\nНа протяжении курса мы будем создавать систему для продажи билетов в кинотеатре\nчерез интернет. Бизнес-анализ тоже будет присутствовать, но в очень ограниченном\nварианте. Основной упор на то, как писать код.\n\n## Основные темы\n\nПо пути разберём много страшных слов, и я понимаю, что многие вещи, о которых будет говориться,\nвызовут ещё больше вопросов, чем ответов. Цель этого курса показать новые горизонты,\nа не дать всеобъемлющее руководство к действию. Этим курсом ваш путь только начинается.\n\n* Domain-Driven Design\n* Entity, Value-Object\n* Repository\n* Service Layer\n* Inversion Of Control\n* Dependency Inversion Principle\n* Dependency Injection Container\n* FSM\n\n## Дополнительные темы\n\nВ процессе используем множество разных библиотек, таких как:\n\n* bottlejs\n* uuid/validate.js\n* lodash/date-fns\n"},"id":139,"slug":"js-ddd","challenges_count":3,"name":"JS: Предметно-ориентированное проектирование","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы изучите предметно-ориентированное программирование. Вы узнаете больше об инверсии зависимостей и репозиториях. В итоге научитесь использовать шаблон Service Layer для разделения кода на слои. Знания из этого курса помогают программистам выделять правильные сущности и находить связи между ними.","kind":"advanced","updated_at":"2026-01-20T11:54:39.971Z","language":"javascript","duration_cache":30060,"skills":["Использовать предметно-ориентированный дизайн в своей повседневной практике","Грамотно переносить логику предметной области на код (сущности, сервисы)","Правильно строить архитектуру сложных бизнес-приложений, разделять код на слои в соответствии с шаблоном Service Layer","По максимуму использовать возможности ООП для организации легко расширяемого и тестируемого кода"],"keywords":["сущности","сервисы","репозитории","валидация","инверсия зависимостей"],"lessons_count":8,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTEzMiwicHVyIjoiYmxvYl9pZCJ9fQ==--1b8ef92e1ca01464452e31b4419760772ddac0c9/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--39ba06fa99226096df9fc6bb31f84e1d29ea98e9/image.png"},"recommendedLandings":[{"stack":{"id":71,"slug":"js-domain-driven-design","title":"DDD на Javascript","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"finished","order":4700,"duration_in_months":2},"id":127,"slug":"js-domain-driven-design","title":"DDD на Javascript","subtitle":"Навык ООП и предметно-ориентированного программирования для создания масштабируемого кода и карьерного роста","subtitle_for_lists":"Изучите ООП и DDD для создания масштабируемого кода","locale":"ru","current":true,"duration_in_months_text":"2 месяца","stack_slug":"js-domain-driven-design","price_text":"от 3 900 ₽","duration_text":"2 месяца","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAwNywicHVyIjoiYmxvYl9pZCJ9fQ==--f0b38f0e25ed59255acec6eaeaeec0a99aec453f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Binary%20code-rafiki.png"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/js-ddd/lessons/architecture/theory_unit","version":"0b0c6d4ebbd40fd58630a0dd89cc25544ccdf24e","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">JS: Предметно-ориентированное проектирование</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: Архитектура</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Архитектура","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"JS: Предметно-ориентированное проектирование"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><p>При разработке приложений с богатой предметной областью во весь рост встаёт вопрос о том, как правильно организовать код приложения, а если смотреть шире, то какую выбрать архитектуру. О том, какие варианты может предложить вам индустрия мы сейчас и поговорим, но перед тем, как я расскажу про существующие подходы, важно запомнить несколько вещей.</p>
<p>Не существует единственного верного подхода при организации вашего приложения. Известные подходы — всего лишь видение конкретных людей для конкретных ситуаций и конкретных стеков (язык + инструментарий). Любая хорошая архитектура базируется на фундаментальных законах и принципах. Большую часть из них вы уже знаете:</p>
<ul>
<li>Изоляция побочных эффектов;</li>
<li>Хорошая абстракция (абстракция данных, композиция, разделение);</li>
<li>Сильные барьеры (между абстракциями);</li>
<li>Слабые связи (возможность замены/независимого развития).</li>
</ul>
<p>Из популярного можно выделить следующие словосочетания:</p>
<ul>
<li>The Clean Architecture</li>
<li>Onion Architecture</li>
<li>Hexagonal Architecture</li>
</ul>
<p>Все эти архитектуры сводятся так или иначе к тому, что наше приложение представляет из себя набор слоёв (тех самых абстракций), которые связаны друг с другом определённым образом и отвечают за определённые аспекты системы. Лучше всего начать анализ с красивой картинки:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTE0MCwicHVyIjoiYmxvYl9pZCJ9fQ==--b252dc8fd1a9b99a0138f6dd7d75b4edc9695033/architecture.png" alt="architecture" loading="lazy"/></p>
<p>Начать стоит с того, что: <em>Фреймворк — это не ваше приложение</em>. Остановитесь на секундочку и хорошо обдумайте фразу. В типичных веб-приложениях фреймворк определяет вообще всё. Приложение на 100% переплетается с ним и становится его частью. Программист начинает мыслить в рамках возможностей фреймворка и его ограничений, и в его голове появляются несуществующие причинно-следственные связи.</p>
<p>Да, конечно, сложно (и не нужно) делать абсолютную изоляцию, но и всегда нужно проводить чёткую грань между приложением и тем фреймворком, который вы используете.</p>
<h2 id="heading-2-1">Домен</h2>
<p>Первым и базовым слоем в приложении является Домен. Это реализация вашей модели предметной области. Чистая бизнес-логика без намёка на инфраструктуру.</p>
<p>Вот что обычно характеризует домен:</p>
<ul>
<li>Чистый код (pure)</li>
<li>Plain Old X Object (POXO)</li>
<li>Бизнес-логика</li>
<li>Валидация</li>
</ul>
<p><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">POXO</code> - это обобщённое название, которое в каждом конкретном языке приобретает своё собственное имя. В Java <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">POJO</code>, в Ruby <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">PORO</code>, и так далее. Этой аббревиатурой описывают объекты, которые построены исключительно на возможностях самого языка, без дополнительных абстракций. Так подчёркивается, что домен не использует внешних библиотек, которые влияют на его организацию. Не надо фанатично относиться к этой идее. В некоторых языках сформировались свои правила, и они идут вразрез с общими концепциями.</p>
<h2 id="heading-2-2">Персистентность</h2>
<p>Реализовать логику только половина дела. В конце концов нужно сохранить наши изменения. Казалось бы, что эта часть должна быть самой простой, но нет. Состояние, его изменение и поддержка целостности настолько сложная история, что придуманы огромные и сложные фреймворки, называемые <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">ORM</code>. Обычно они построены вокруг двух самых распространённых паттернов:</p>
<ul>
<li>ActiveRecord</li>
<li>DataMapper</li>
</ul>
<p>В этом курсе мы их не рассматриваем, но в реальной жизни вам придётся с ними столкнуться.</p>
<h2 id="heading-2-3">Репозиторий</h2>
<p>Репозиторий — это хранилище однотипных сущностей. Позволяет как делать выборки, так и сохранять сущности внутри себя. Для простоты в нашем приложении репозитории будут хранить все данные в памяти.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">const film = new Film(name, duration)
repository = new FilmRepository()
repository.save(film)
repository.find(film.id) // film
repository.find(unknownId) // Boom!</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<h2 id="heading-2-4">Инфраструктура</h2>
<p>Именно в эту категорию попадает фреймворк, UI и вообще любая прикладная история. На картинке этот слой находится на самой внешней стороне. Из него происходит отправка электронных писем, смс, и выполняется так называемая логика приложения. Например, перенаправление на определённую страницу после создания какой-то сущности.</p>
<h2 id="heading-2-5">Сервисы</h2>
<p>Помните диаграмму вариантов использования? Вот именно эти варианты и являются единственным способом изменения состояния вашего приложения. Ваш домен обрастает слоем так называемых сервисов. Каждый сервис представляет собой набор функций, имитирующих бизнес-сценарии, например, "добавить в друзья", "поставить лайк".</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">class CinemaService {
createFilm(name, duration) {
const film = new Film(name, duration)
this.FilmRepository.save(film)
return film
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Инфраструктурный слой является главным пользователем вашего слоя сервисов. Сервисы могут вызываться в <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">ui</code>, в контроллерах, в асинхронных обработчиках. Слой сервисов настолько важен сам по себе, что Мартин Фаулер описывает его как шаблон проектирования <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://martinfowler.com/eaaCatalog/serviceLayer.html" rel="noopener noreferrer" target="_blank">Service Layer</a></p>
<p>При проектировании сервисов нужно придерживаться некоторых правил, которые позволяют абстракции не протекать и максимально поддерживать чистоту.</p>
<p>Входными данными в функции сервиса не могут быть сущности предметной области. Причина такого правила очень проста. Сервисы – слой поверх предметной области, он инкапсулирует в себе все сценарии. Если сущности окажутся снаружи, то логика становится размазанной между слоями (потекла абстракция), пропадает изоляция. Но так сделать не всегда возможно. Иногда это связано с устройством конкретных фреймворков, которые не дают нормально абстрагировать предметную область от инфраструктуры. В такой ситуации не стоит бороться насмерть за концептуальную чистоту, идите на компромиссы.</p>
<p>То же самое касается выходных данных. В теории, отдавать наружу сущности нельзя по той же причине, по которой нельзя ими оперировать вне сервисов. Так как после возврата крайне просто начать ей оперировать, что сразу повлечёт за собой размазывание логики по слоям. Вместо сущности, как правило, отдают специальный "Data Transfer Object". В отличие от сущности он не содержит поведения и используется исключительно как контейнер для чтения.</p>
<blockquote>
<p>DTO - Используется для передачи данных между подсистемами приложения. DTO, в отличие от business object, не должен содержать какого-либо поведения</p>
</blockquote>
<ul>
<li>Неизменяемый</li>
<li>Просто данные</li>
</ul>
<p>Опять же, чтобы не усложнять, в тех системах, где нет готовых механизмов для трансляции, возвращают и сущности, но на уровне соглашений используют их исключительно как <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">DTO</code>.</p>
<p>И последнее по списку, но не последнее по важности, не вызывайте сервисы из сервисов. Если появляется общий код, то выносите общую функциональность, но не позволяйте самому сервису начинать мешаться с доменом. Последнее означает то, что если сервисы начинают использовать внутри себя сервисы (тот же или другие), то с большой вероятностью происходит нарушение принципа одного уровня абстракции. Сервисы — слой <em>поверх</em> домена, а это значит, что на одном уровне нельзя использовать и домен, и сервис.</p></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/js-domain-driven-design?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">2 месяца</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">DDD на Javascript</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите ООП и DDD для создания масштабируемого кода</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAwNywicHVyIjoiYmxvYl9pZCJ9fQ==--f0b38f0e25ed59255acec6eaeaeec0a99aec453f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Binary%20code-rafiki.png" alt="DDD на Javascript" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/js-ddd/lessons/architecture/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 8</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/js-ddd/lessons/architecture/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-CdBlNCiQ.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>