<!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 16:54:21 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="6wKlnGRUTRD1uJZrMdvBSXsXUrWb02c4lVa3TVSunuUE026rlirgcEP7svM91DE-ux5_H5PkmZooti0ZBql5iw";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/entity-relation/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/entity-relation/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="IHGwuqHZYFUQ-cZU1BOaWoVP2W4mx5vILUpI0ZdYXb7PoHuNU6fNNaa64szYHGotRUb0xC7wZWqQqtKFxV-60A" />
<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-26T16:54:21.242Z","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":"lA_xCU0ZAy5Ro31Ah9potLBrSgvivmh2FSfkZsiGXVt73jo-v2euTufgWdiL1ZjDcGJnoeqJltSox34ymoG6NQ","topics":[{"id":47540,"title":"``` this.cinemaHall.addFilmScreening(this); ``` Обьясните пожалуйсто это что такое и с чем его едят... Тем более что ``` addFilmScreening ``` метод ``` CinemaHall ``` - а. Для обучающихся \\то выглядит как магия - а не как уже усвоенный материал... Или, подскажите, где тот Хогварц где этому учат.....","plain_title":"this.cinemaHall.addFilmScreening(this); Обьясните пожалуйсто это что такое и с чем его едят... Тем более что addFilmScreening метод CinemaHall - а. Для обучающихся \\то выглядит как магия - а не как уже усвоенный материал... Или, подскажите, где тот Хогварц где этому учат..... ","creator":{"public_name":"Igor Izbish","id":245176,"is_tutor":false},"comments":[{"creator":{"public_name":"Igor Izbish","id":245176,"is_tutor":false},"id":102266,"body":"**Igor Izbish**, \nнашел овет \n``` \nconst cinemaHall = new CinemaHall(cinemaHallName, rows, cols);\nconst filmScreening = new FilmScreening(film, cinemaHall, time);\n\n```\n\nвопрос снят","topic_id":47540}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":51104,"title":"В тестах к теории есть вопрос: \n> Какой тип связи у пары библиотечная книга-читатель?\n\nОбъясните пожалуйста почему ответ - m2m.\n\n> Например, человек может быть владельцем нескольких машин, но машина может принадлежать только одному человеку\n\nКнига может принадлежать только одному читателю, её не могут одновременно взять несколько человек (если речь не о цифровом формате).\n\nНо с другой стороны владельцев книги за определённый период времени было несколько.\nПеренося на пример с машиной: если машину продать, то у неё было несколько владельцев за определённый период времени.","plain_title":"В тестах к теории есть вопрос: Какой тип связи у пары библиотечная книга-читатель? Объясните пожалуйста почему ответ - m2m. Например, человек может быть владельцем нескольких машин, но машина может принадлежать только одному человеку Книга может принадлежать только одному читателю, её не могут одновременно взять несколько человек (если речь не о цифровом формате). Но с другой стороны владельцев книги за определённый период времени было несколько. Перенося на пример с машиной: если машину продать, то у неё было несколько владельцев за определённый период времени. ","creator":{"public_name":"Адель","id":63047,"is_tutor":false},"comments":[{"creator":{"public_name":"Sergei Melodyn","id":162475,"is_tutor":true},"id":109300,"body":"**Адель**, приветствую.\n\nЕсли вы поднимите историю книги, то обнаружите, что у неё было много владельцев, пусть и в разные периоды времени. К машине это тоже относится. Именно с точки зрения организации базы данных связь m2m будет правильной. ","topic_id":51104}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":45609,"title":"Можно ли обойтись без мутации и в CinemaHall а именно в addFilmScreening? Вроде как учили что мутация это зло, а тут такое! Понятно что если не мутировать то не получится в FilmScreening внутри к залу привязывать filmScreening, но может есть какой-то вариант без мутации?","plain_title":"Можно ли обойтись без мутации и в CinemaHall а именно в addFilmScreening? Вроде как учили что мутация это зло, а тут такое! Понятно что если не мутировать то не получится в FilmScreening внутри к залу привязывать filmScreening, но может есть какой-то вариант без мутации? ","creator":{"public_name":"Factory Factory","id":184938,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":98670,"body":"Не получится. Мутации это жизнь) Главное делать их только там где нужно и так чтобы это не ломало систему.\n\np.s. Без мутаций бывает в фп языках, но там свои сложности добавляются.","topic_id":45609}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":21174,"title":"я так понимаю библиотека `uuid-js` немного обновилась с момента написания курса, теперь чтобы получить hex-представление объекта-идентификатора в документации указывается метод `toString()`:\n\n```\nMETHOD LIST\nuuid.toString(); // hex string version of UUID\n```","plain_title":"я так понимаю библиотека uuid-js немного обновилась с момента написания курса, теперь чтобы получить hex-представление объекта-идентификатора в документации указывается метод toString(): METHOD LIST uuid.toString(); // hex string version of UUID ","creator":{"public_name":"Nikita","id":184506,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":44831,"body":"Поправил, спасибо!","topic_id":21174}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":27896,"title":"Может сейчас какую-то глупость спрошу, но насколько семантически верно что, показ фильма не только знает о зале, но и модифицирует его? Вроде как нарушается изоляция сущностей/модулей.","plain_title":"Может сейчас какую-то глупость спрошу, но насколько семантически верно что, показ фильма не только знает о зале, но и модифицирует его? Вроде как нарушается изоляция сущностей/модулей. ","creator":{"public_name":"Davud Kakhrimanov","id":139303,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":60027,"body":"Показ это конкретная штука привязанная к конкретному залу и времени.","topic_id":27896},{"creator":{"public_name":"Dmitriy Bataev","id":106130,"is_tutor":false},"id":59903,"body":"Здравствуйте. Тут нужно четко понимать, что из себя представляет этот \"показ фильма\". Если речь идет о сеансе в кинотеатре в целом, то конечно зал, где он проходит, и фильм, который в нем показывается, являются не только его частью, но и заменяемы (номер зала, конкретный фильм, время показа, стоимость билета и т.д.)","topic_id":27896}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":21410,"title":"Ниже топик уже есть, что обновили `uuid-js`, но в теории и местами в решение учителя, старый метод `.hex`","plain_title":"Ниже топик уже есть, что обновили uuid-js, но в теории и местами в решение учителя, старый метод .hex ","creator":{"public_name":"Sergey Tabb","id":148171,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":45400,"body":"fixed!","topic_id":21410}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":37469,"title":"Всё написал сам, но ловил ошибку TypeError: _entities.Film is not a constructor\nНе мог понять, в чем проблема, пришлось посмотреть решение учителя. Оказалось, что сущности нужно экспортировать по умолчанию. Так и сделал, всё заработало. Но честно говоря это не было очевидно. В задании сказано, что solution нужно экспортировать по умолчанию, а про сущности просто \"экспортируйте\".","plain_title":"Всё написал сам, но ловил ошибку TypeError: _entities.Film is not a constructor Не мог понять, в чем проблема, пришлось посмотреть решение учителя. Оказалось, что сущности нужно экспортировать по умолчанию. Так и сделал, всё заработало. Но честно говоря это не было очевидно. В задании сказано, что solution нужно экспортировать по умолчанию, а про сущности просто \"экспортируйте\". ","creator":{"public_name":"Глазик","id":224260,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Chertkov","id":130487,"is_tutor":false},"id":81947,"body":"Здравствуйте, Вадим!\n\nДополнил ридми упражнения. Спасибо!","topic_id":37469}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":40235,"title":"Добрый день!\nПо невнимательности сначала сделал решение в котором в свойство filmScreenings добавлял фильм и класс Film реализовал так, что бы проходили тесты. Соответственно прошло ошибочное решение - https://ru.hexlet.io/code_reviews/239130.\n\nМожет добавить в тесты проверку на тип объекта, который содержится в filmScreenings.\nНапример, такой модифицированный тест\n```\n// @ts-check\n\nimport solution from '../solution';\nimport FilmScreening from '../entities/FilmScreening';\n\ntest('solution1', () => {\n const time = new Date();\n const filmScreening = solution('snack', 150, 'smily hall', 30, 50, time);\n const expected = {\n time,\n film: {\n name: 'snack',\n duration: 150,\n },\n };\n expect(filmScreening).toMatchObject(expected);\n expect(filmScreening.cinemaHall).toMatchObject({\n name: 'smily hall',\n rows: 30,\n cols: 50,\n filmScreenings: [{ time }],\n });\n const [firstFilmScreening] = filmScreening.cinemaHall.filmScreenings;\n expect(firstFilmScreening).toBeInstanceOf(FilmScreening);\n});\n\ntest('solution2', () => {\n const time = new Date();\n const filmScreening = solution('the game', 230, 'number 2', 80, 30, time);\n const expected = {\n time,\n film: {\n name: 'the game',\n duration: 230,\n },\n };\n expect(filmScreening).toMatchObject(expected);\n expect(filmScreening.cinemaHall).toMatchObject({\n name: 'number 2',\n rows: 80,\n cols: 30,\n filmScreenings: [{ time }],\n });\n const [firstFilmScreening] = filmScreening.cinemaHall.filmScreenings;\n expect(firstFilmScreening).toBeInstanceOf(FilmScreening);\n});\n\n```","plain_title":"Добрый день! По невнимательности сначала сделал решение в котором в свойство filmScreenings добавлял фильм и класс Film реализовал так, что бы проходили тесты. Соответственно прошло ошибочное решение - https://ru.hexlet.io/code_reviews/239130. Может добавить в тесты проверку на тип объекта, который содержится в filmScreenings. Например, такой модифицированный тест ``` // @ts-check import solution from '../solution'; import FilmScreening from '../entities/FilmScreening'; test('solution1', () => { const time = new Date(); const filmScreening = solution('snack', 150, 'smily hall', 30, 50, time); const expected = { time, film: { name: 'snack', duration: 150, }, }; expect(filmScreening).toMatchObject(expected); expect(filmScreening.cinemaHall).toMatchObject({ name: 'smily hall', rows: 30, cols: 50, filmScreenings: [{ time }], }); const [firstFilmScreening] = filmScreening.cinemaHall.filmScreenings; expect(firstFilmScreening).toBeInstanceOf(FilmScreening); }); test('solution2', () => { const time = new Date(); const filmScreening = solution('the game', 230, 'number 2', 80, 30, time); const expected = { time, film: { name: 'the game', duration: 230, }, }; expect(filmScreening).toMatchObject(expected); expect(filmScreening.cinemaHall).toMatchObject({ name: 'number 2', rows: 80, cols: 30, filmScreenings: [{ time }], }); const [firstFilmScreening] = filmScreening.cinemaHall.filmScreenings; expect(firstFilmScreening).toBeInstanceOf(FilmScreening); }); ","creator":{"public_name":"Степан Рябухин","id":227519,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":88194,"body":"Спасибо, тесты улучшил)","topic_id":40235}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":23344,"title":"Зачем в `filmScreeninig` хранить весь объект `film` и `cinemaHall`, а в `cinemaHall`, весь объект `filmScreeninig`?\nНе проще устанавливать связь по id? В `filmScreening` - `filmId` и `cinemaHallId`, в `cinemaHall` - `filmScreeningId`.","plain_title":"Зачем в filmScreeninig хранить весь объект film и cinemaHall, а в cinemaHall, весь объект filmScreeninig? Не проще устанавливать связь по id? В filmScreening - filmId и cinemaHallId, в cinemaHall - filmScreeningId. ","creator":{"public_name":"Victor Shabratsky","id":110214,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":49813,"body":"На уровне использования этой абстракции мы должны оперировать сущностями, а не идентификаторами, а то, как оно внутри, уже зависит от конкретного подхода в конкретной экосистеме.","topic_id":23344}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}},{"id":8413,"title":"Как у вас в тестах свойство CinemaHall.filmScreening содержит только значение time, хотя по ссылке мы даем this , то есть он должен выводить все свойства обьекта FilmScreening.\n ","plain_title":"Как у вас в тестах свойство CinemaHall.filmScreening содержит только значение time, хотя по ссылке мы даем this , то есть он должен выводить все свойства обьекта FilmScreening. ","creator":{"public_name":"Evgeniy Blackbeard","id":99250,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":16429,"body":"матчер `toMatchObject` проверяет не точное соответствие, а только то что передали в него.","topic_id":8413}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Сущности и связи","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":587,"slug":"js_ddd_entities_exercise","name":null,"state":"active","kind":"exercise","language":"javascript","locale":"ru","has_web_view":false,"has_test_view":false,"reviewable":true,"readme":"### entities/Film.js\n\nРеализуйте и экспортируйте по умолчанию сущность `Film`.\n\nПример использования:\n\n```javascript\nconst film = new Film(name, duration);\n```\n\nСвойства:\n\n* `id` - идентификатор (автогенерируемое)\n* `name` - название фильма\n* `duration` - продолжительность\n* `createdAt` - дата создания сущности\n\n### entities/CinemaHall.js\n\nРеализуйте и экспортируйте по умолчанию сущность `CinemaHall`.\n\n```javascript\nconst cinemaHall = new CinemaHall(name, rows, cols);\n```\n\nСвойства:\n\n* `id` - идентификатор (автогенерируемое)\n* `name` - название зала\n* `rows` - количество рядов с сидениями\n* `cols` - количество сидений в ряду\n* `filmScreenings` - все добавленные сеансы\n* `createdAt` - дата создания сущности\n\n### entities/FilmScreening.js\n\nРеализуйте и экспортируйте по умолчанию сущность `FilmScreening`.\n\n```javascript\nconst filmScreening = new FilmScreening(film, cinemaHall, time);\n```\n\nСвойства:\n\n* `id` - идентификатор (автогенерируемое)\n* `film` - фильм\n* `cinemaHall` - зал\n* `time` - время сеанса\n* `createdAt` - дата создания сущности\n\nВо время создания сеанса, нужно добавлять сеанс в `cinemaHall`.\n\n### solution.js\n\nРеализуйте и экспортируйте функцию по умолчанию, которая создает просмотр фильма `FilmScreening`\n\n```javascript\nconst filmScreening = solution('snack', 150, 'smily hall', 30, 50, time);\n{\n time: 150,\n film: {\n name: 'snack',\n duration: 150\n },\n cinemaHall: {\n name: 'smily hall',\n rows: 30,\n cols: 50,\n filmScreenings: [[Object]],\n }\n}\n```\n","prepared_readme":"### entities/Film.js\n\nРеализуйте и экспортируйте по умолчанию сущность `Film`.\n\nПример использования:\n\n```javascript\nconst film = new Film(name, duration);\n```\n\nСвойства:\n\n* `id` - идентификатор (автогенерируемое)\n* `name` - название фильма\n* `duration` - продолжительность\n* `createdAt` - дата создания сущности\n\n### entities/CinemaHall.js\n\nРеализуйте и экспортируйте по умолчанию сущность `CinemaHall`.\n\n```javascript\nconst cinemaHall = new CinemaHall(name, rows, cols);\n```\n\nСвойства:\n\n* `id` - идентификатор (автогенерируемое)\n* `name` - название зала\n* `rows` - количество рядов с сидениями\n* `cols` - количество сидений в ряду\n* `filmScreenings` - все добавленные сеансы\n* `createdAt` - дата создания сущности\n\n### entities/FilmScreening.js\n\nРеализуйте и экспортируйте по умолчанию сущность `FilmScreening`.\n\n```javascript\nconst filmScreening = new FilmScreening(film, cinemaHall, time);\n```\n\nСвойства:\n\n* `id` - идентификатор (автогенерируемое)\n* `film` - фильм\n* `cinemaHall` - зал\n* `time` - время сеанса\n* `createdAt` - дата создания сущности\n\nВо время создания сеанса, нужно добавлять сеанс в `cinemaHall`.\n\n### solution.js\n\nРеализуйте и экспортируйте функцию по умолчанию, которая создает просмотр фильма `FilmScreening`\n\n```javascript\nconst filmScreening = solution('snack', 150, 'smily hall', 30, 50, time);\n{\n time: 150,\n film: {\n name: 'snack',\n duration: 150\n },\n cinemaHall: {\n name: 'smily hall',\n rows: 30,\n cols: 50,\n filmScreenings: [[Object]],\n }\n}\n```\n","has_solution":true,"entity_name":"Сущности и связи"},"units":[{"id":1734,"name":"theory","url":"/courses/js-ddd/lessons/entity-relation/theory_unit"},{"id":2490,"name":"quiz","url":"/courses/js-ddd/lessons/entity-relation/quiz_unit"},{"id":1736,"name":"exercise","url":"/courses/js-ddd/lessons/entity-relation/exercise_unit"}],"links":[],"ordered_units":[{"id":1734,"name":"theory","url":"/courses/js-ddd/lessons/entity-relation/theory_unit"},{"id":2490,"name":"quiz","url":"/courses/js-ddd/lessons/entity-relation/quiz_unit"},{"id":1736,"name":"exercise","url":"/courses/js-ddd/lessons/entity-relation/exercise_unit"}],"id":849,"slug":"entity-relation","state":"approved","name":"Сущности и связи","course_order":20,"goal":"Разбираемся, что такое сущности и как работают связи между сущностями","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Настал момент, когда нужно начинать проектировать приложение. И делать мы это будем, используя `Entity-relationship Model`\n\n> ERM - Модель данных, позволяющая описывать концептуальные схемы предметной области.\n\n## Сущности\n\nЭтот подход включает в себя два основных понятия: сущность и связь. Проще всего начать с примеров:\n\n* Пользователь\n* Кинозал\n* Фильм\n* Билет\n* Показ фильма\n\nЭто сущности нашей предметной области, с которыми предстоит работать в коде. Как видите, понятие сущность довольно интуитивно. Но также оно обладает и рядом формальных характеристик:\n\n* Идентификация\n* Время жизни\n\nИдентификация означает, что мы можем рассматривать сущности независимо и выделять одни среди других. Например, у нас есть разные кинозалы, и это разные сущности. Другой пример это пользователи. Даже если два человека имеют одинаковые ФИО, мы всё равно сможем их различить на основе дополнительных признаков. В программировании обычно сущностям присваивается идентификатор (суррогатный ключ), который и используется для этой цели. Чаще всего эта задача возлагается на базу данных. В нашей ситуации базы нет, поэтому мы будем задавать его самостоятельно.\n\n```javascript\nconst user = new User('Илон')\nconsole.log(user.id)\n// 896b677f-fb14-11e0-b14d-d11ca798dbac\n\n// User.js\nimport uuid from 'uuid/v4'\n\nclass User {\n constructor(name) {\n this.id = uuid()\n this.name = name\n }\n}\n```\n\nБиблиотека `uuid` позволяет генерировать уникальный идентификатор, который можно использовать для идентификации. Кстати [uuid](https://ru.wikipedia.org/wiki/UUID) очень полезная штука, может пригодиться в некоторых типах задач.\n\nВремя жизни означает, что наша сущность в какой-то момент появилась и когда-то может исчезнуть.\n\n## Связи\n\nМежду собой сущности образуют связи. Например, человек может быть владельцем нескольких машин, но машина может принадлежать только одному человеку. Пользователи Хекслета проходят много курсов, каждый курс доступен всем пользователям.\n\nТаким образом можно выделить три основных типа связи: один к одному (o2o), один ко многим (o2m) и многие ко многим (m2m).\n\n\n\nВыше представлена диаграмма Entity-Relationship. Она входит в стандарт _UML_ и неплохо помогает понять то, какие сущности составляют вашу предметную область и как они друг с другом связаны.\n\nЧто можно сказать глядя на диаграмму?\n\n* В одном зале может быть много показов фильмов;\n* Один фильм может быть показан много раз;\n* Фильмы и залы связаны друг с другом как \"многие ко многим\". То есть один фильм показывается в разных залах, а в одном зале идут разные фильмы.\n\nВсё это довольно очевидно и соответствует нашему опыту посещения кинозалов. В других предметных областях это уже не так просто, и то, как вы проектируете сущности и их связи, имеет сильное влияние на ваше приложение. Общее правило такое, чем больше связей и чем более они разнообразные, тем сложнее приложение. Часто бывает такое, что программисты \"закладываются на будущее\" (которое не факт, что наступит) и пытаются делать чуть ли не все связи _m2m_. Чаще всего такой подход оказывается примером _over-engineering_ (гиперпроектирование), другими словами, не надо добавлять сложности там, где нет реальной потребности.\n\nКроме влияния на логику работы, связи также сильно влияют на способ хранения сущностей в базе данных. Например, в реляционных базах данных, связь _m2m_ всегда подразумевает наличие промежуточной таблицы. В свою очередь рефакторинг базы данных не такое простое занятие, как изменение кода.\n\n## Пример\n\nНа Хекслете есть курсы. Каждый курс состоит из уроков. Урок не может существовать без курса. Вот как может быть представлена эта модель в коде:\n\n```javascript\nconst course = new Course('JS: DDD')\nconst lesson1 = new Lesson(course, 'Введение')\nconst lesson2 = new Lesson(course, 'Модель Сущность-Связь')\n```\n\nПередача курса в конструктор удобна по двум причинам. Сразу становится видна и понятна связь урока с курсом. А также на уровне языка заложено бизнес-правило, что урок не может существовать без курса.\n\n## Объекты-значения (Справочники)\n\nКроме сущностей в предметной области всегда есть и значения, или, как их обычно называют, объекты-значения. В отличие от сущностей у них нет идентификации. Возьмём такое понятие как деньги (Money). Если мы не являемся казначейством, то нужно ли нам отличать одни `100$` от других `100$`? Вероятно, нет. Для нас не существует сущности `100$`, всё, что имеет значение, это номинальная стоимость этих денег, другими словами, в случае объектов-значений сравнение происходит не по идентификации, а на основе фактического значения. То же самое применимо ко всем справочным данным. Имена стран (производители фильмов), адреса, список городов и многое другое.\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/entity-relation/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>Настал момент, когда нужно начинать проектировать приложение. И делать мы это будем, используя <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Entity-relationship Model</code></p>
<blockquote>
<p>ERM - Модель данных, позволяющая описывать концептуальные схемы предметной области.</p>
</blockquote>
<h2 id="heading-2-1">Сущности</h2>
<p>Этот подход включает в себя два основных понятия: сущность и связь. Проще всего начать с примеров:</p>
<ul>
<li>Пользователь</li>
<li>Кинозал</li>
<li>Фильм</li>
<li>Билет</li>
<li>Показ фильма</li>
</ul>
<p>Это сущности нашей предметной области, с которыми предстоит работать в коде. Как видите, понятие сущность довольно интуитивно. Но также оно обладает и рядом формальных характеристик:</p>
<ul>
<li>Идентификация</li>
<li>Время жизни</li>
</ul>
<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 user = new User('Илон')
console.log(user.id)
// 896b677f-fb14-11e0-b14d-d11ca798dbac
// User.js
import uuid from 'uuid/v4'
class User {
constructor(name) {
this.id = uuid()
this.name = name
}
}</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">uuid</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://ru.wikipedia.org/wiki/UUID" rel="noopener noreferrer" target="_blank">uuid</a> очень полезная штука, может пригодиться в некоторых типах задач.</p>
<p>Время жизни означает, что наша сущность в какой-то момент появилась и когда-то может исчезнуть.</p>
<h2 id="heading-2-2">Связи</h2>
<p>Между собой сущности образуют связи. Например, человек может быть владельцем нескольких машин, но машина может принадлежать только одному человеку. Пользователи Хекслета проходят много курсов, каждый курс доступен всем пользователям.</p>
<p>Таким образом можно выделить три основных типа связи: один к одному (o2o), один ко многим (o2m) и многие ко многим (m2m).</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6OTEzOSwicHVyIjoiYmxvYl9pZCJ9fQ==--94543e5a21920938492734d6b3bfc9befc7bee08/erd.png" alt="erd" loading="lazy"/></p>
<p>Выше представлена диаграмма Entity-Relationship. Она входит в стандарт <em>UML</em> и неплохо помогает понять то, какие сущности составляют вашу предметную область и как они друг с другом связаны.</p>
<p>Что можно сказать глядя на диаграмму?</p>
<ul>
<li>В одном зале может быть много показов фильмов;</li>
<li>Один фильм может быть показан много раз;</li>
<li>Фильмы и залы связаны друг с другом как "многие ко многим". То есть один фильм показывается в разных залах, а в одном зале идут разные фильмы.</li>
</ul>
<p>Всё это довольно очевидно и соответствует нашему опыту посещения кинозалов. В других предметных областях это уже не так просто, и то, как вы проектируете сущности и их связи, имеет сильное влияние на ваше приложение. Общее правило такое, чем больше связей и чем более они разнообразные, тем сложнее приложение. Часто бывает такое, что программисты "закладываются на будущее" (которое не факт, что наступит) и пытаются делать чуть ли не все связи <em>m2m</em>. Чаще всего такой подход оказывается примером <em>over-engineering</em> (гиперпроектирование), другими словами, не надо добавлять сложности там, где нет реальной потребности.</p>
<p>Кроме влияния на логику работы, связи также сильно влияют на способ хранения сущностей в базе данных. Например, в реляционных базах данных, связь <em>m2m</em> всегда подразумевает наличие промежуточной таблицы. В свою очередь рефакторинг базы данных не такое простое занятие, как изменение кода.</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 course = new Course('JS: DDD')
const lesson1 = new Lesson(course, 'Введение')
const lesson2 = new Lesson(course, 'Модель Сущность-Связь')</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>Передача курса в конструктор удобна по двум причинам. Сразу становится видна и понятна связь урока с курсом. А также на уровне языка заложено бизнес-правило, что урок не может существовать без курса.</p>
<h2 id="heading-2-4">Объекты-значения (Справочники)</h2>
<p>Кроме сущностей в предметной области всегда есть и значения, или, как их обычно называют, объекты-значения. В отличие от сущностей у них нет идентификации. Возьмём такое понятие как деньги (Money). Если мы не являемся казначейством, то нужно ли нам отличать одни <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">100$</code> от других <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">100$</code>? Вероятно, нет. Для нас не существует сущности <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">100$</code>, всё, что имеет значение, это номинальная стоимость этих денег, другими словами, в случае объектов-значений сравнение происходит не по идентификации, а на основе фактического значения. То же самое применимо ко всем справочным данным. Имена стран (производители фильмов), адреса, список городов и многое другое.</p>
<p>Важно понимать, что это не абсолютная истина. Будет ли какое-то понятие сущностью или значением зависит от конкретной предметной области.</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/entity-relation/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/entity-relation/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>