22 - Архитектура front-end приложения, Separtion of Concerns, Data Access Layer

Оценить качество материала и подачу материала автором видео:

Front-end

Трудоустройтесь middle front-end разработчиком на React JS (TypeScript) за 12-16 месяцев обучения с ежедневной менторской поддержкой в формате видео 1 на 1 и коммерческими проектами в портфолио

Перейти на курс
Front-end

Back-end

Трудоустройтесь middle back-end разработчиком за 12-16 месяцев обучения с ежедневной менторской поддержкой в формате видео 1 на 1 и коммерческими проектами в портфолио

Перейти на курс
Back-end

Карьерный бустер

Получите коммерческий опыт на реальных стартапах, прокачайте tech & soft навыки, научитесь работать в команде, проходить собеседования и получите первую работу в IT!

Перейти на курс
Карьерный бустер

Основы Front-end

Сделайте первый шаг в IT, освоив базовые знания разработки и научившись создавать небольшие проекты на JavaScript

Перейти на курс
Основы Front-end

Основы Back-end

Сделайте первый шаг в IT, освоив базовые знания разработки. Без опыта. Без математики. Только практика: JavaScript, SQL, Node JS, база данных

Перейти на курс
Основы Back-end

Архитектура React-приложений и Data Access Layer (DAL)

Автор конспекта: Арина Василевская

Введение в архитектурные концепции

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

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

Основные принципы и модели

principles

1. Клиент-серверная модель

  • Клиент запрашивает ресурсы или сервисы;
  • Сервер предоставляет эти ресурсы и обрабатывает запросы.

Клиентом является браузер, а сервером — backend. Но backend, в свою очередь, может быть клиентом для другого сервера — например, базы данных. Так реализуется взаимодействие независимых процессов (межпроцессная коммуникация).

2. Информационный эксперт

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

3. Инверсия управления (Inversion of Control, IoC)

Происходит, когда библиотека или фреймворк управляет нашим кодом. Например, React вызывает компонент для перерисовки в нужный момент жизненного цикла, что делает его фреймворком в контексте IoC. Каждый раз, передавая callback, мы фактически создаём локальную инверсию управления. Дочерние компоненты могут управлять поведением родителя через callback, хотя сами не зависят от родителя напрямую (нет импортов на родителя).

4. Принцип единственной ответственности (Single Responsibility Principle, SRP)

«У модуля должна быть только одна причина для изменения.»

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

⚠️

Во многих React-проектах один компонент совмещает несколько разных ролей:

  1. Запросы к серверу (коммуникация с API).
  2. Логика обработки данных (преобразование, фильтрация, вычисления).
  3. Отрисовка UI (визуальное представление и обработка событий).

Такой компонент нарушает принцип SRP и становится «толстым» (fat/thick component). Это затрудняет тестирование, повторное использование и сопровождение кода.

5. Разделение ответственностей (Separation of Concerns)

Separation of Concerns (SoC) — это архитектурный принцип, согласно которому система должна быть разделена на независимые аспекты (зоны ответственности) (concerns): отрисовка интерфейса, управление состоянием, сетевые запросы, бизнес-логика и т.д. Каждый concern должен решать одну задачу и знать минимум о других.

SoCSchema

Основные слои:

📌

Слой (layer) — это практическое воплощение определённого concern в структуре проекта, например, в виде папки.

  1. 🖥️ Presentation Layer — презентационный слой — отвечает за отображение данных и взаимодействие с пользователем (UI). Идеально, если этот слой «тонкий»: содержит только код интерфейса и не выполняет бизнес-логику.

  2. ⚙️ Business Logic Layer (BLL) — слой бизнес-логики — управляет состоянием и данными, реализует правила и алгоритмы.

  3. 🗃️ Data Access Layer (DAL) — отвечает за работу с API и хранилищами. Содержит логику запросов и не зависит от фреймворков, что позволяет переиспользовать его, например, и в React, и в Angular проектах.

✅ Преимущества разделения:

  • Упрощает масштабирование и поддержку проекта.
  • Повышает гибкость и переносимость кода.
  • Позволяет переиспользовать модули в разных архитектурах.
  • Снижает связанность компонентов и делает систему устойчивой к изменениям.
💡

Чем меньше слои знают друг о друге — тем проще развивать и адаптировать систему без глобальных переделок.

Практический рефакторинг: создание DAL

📌

Рефакторинг — это улучшение структуры кода на основе архитектурных принципов без изменения видимого поведения программы для пользователя.

Создание структуры

Разделим структуру нашего приложения на два слоя: слой пользовательского интерфейса (UI) вместе с управлением состоянием (state management) и слой доступа к данным (Data Access Layer).

  • В папке /components находятся компоненты. А в React компоненты, как мы знаем, отвечают за пользовательский интерфейс (UI). Чтобы подчеркнуть этот аспект ещё яснее, можно переименовать папку в /ui. После переименования папка четко отражает свой функциональный аспект — слой интерфейса.

  • Создадим новую папку /dal с файлом api.ts внутри. Этот слой будет отвечать за работу с API и хранилищами

      • api.ts
  • Реализация функции getTrack

    Сейчас компонент TrackDetail напрямую выполняет сетевой запрос. Любое изменение, например, API-ключа, требует правки внутри UI-кода, что противоречит его назначению — отрисовка интерфейса. Поэтому вынесем логику запроса в api.ts.

    api.ts
    export const getTrack = (trackId: string) => {
      const promise = fetch("https://musicfun.it-incubator.app/api/1.0/playlists/tracks/" + trackId, {
        headers: {
          "api-key": "28ea...", // ❗Подставь свой API-KEY
        },
      }).then((res) => res.json())
     
      return promise
    }

    Использование в компоненте

    Компоненту не нужно знать детали реализации запроса (адрес, метод, API-ключ). Ему достаточно вызвать функцию и получить данные в ответ. Логика запроса (fetch, преобразование JSON и т.д.) скрыта внутри DAL, заменим код запроса на вызов функции getTrack:

    TrackDetail.tsx
    //...
     
    useEffect(() => {
      //...
      getTrack(trackId).then((json) => setSelectedTrack(json.data))
    }, [trackId])
     
    //...
    • Компонент вызывает getTrack и подписывается на Promise через .then().
    • В обработчик .then() приходят данные, которые затем устанавливаются в локальный стейт. Это снова демонстрирует инверсию управления, так как Promise вызывает нашу callback-функцию.

    ✅️ Результат рефакторинга

    • Компонент становится тонким — вместо множества строк сетевого кода остаётся 1–2 строки вызова.
    • Компонент не знает, откуда берутся данные (сервер, кэш и т.п.), что повышает гибкость и тестируемость.
    • В проекте появляется одно место — api.ts, куда мы идём по единственной причине: если изменился контракт взаимодействия с бэкендом — типизация, эндпоинт или API-ключ.

    Типизация

    Типизация — один из ключевых элементов архитектуры, обеспечивающий предсказуемость, надёжность и прозрачность структуры кода. В контексте Data Access Layer (DAL) типизация помогает чётко определить, что именно возвращает функция, выполняющая запрос, и какая структура данных ожидается от сервера.

    Типизируем Promise, возвращаемые функцией getTrack, для этого используем тип GetTrackDetailsOutputData, который импортируем из TrackDetail.tsx:

    api.ts
    export const getTrack = (trackId: string) => {
      const promise: Promise<{ data: GetTrackDetailsOutputData }> = fetch(
        "https://musicfun.it-incubator.app/api/1.0/playlists/tracks/" + trackId,
        {
          headers: {
            "api-key": "28ea...", // ❗Подставь свой API-KEY
          },
        },
      ).then((res) => res.json())
     
      return promise
    }

    Мы столкнулись с проблемой цикличности импортов - компонент импортирует функцию из DAL, а DAL импортирует тип из компонента.

    Решение: перенести тип в api.ts:

    api.ts
    export type GetTrackDetailsOutputData = {
      id: string
      attributes: {
        title: string
        lyrics: string | null
      }
    }
     
    export const getTrack = (trackId: string) => {
      const promise: Promise<{ data: GetTrackDetailsOutputData }> = fetch().then((res) => res.json())
      //...
     
      return promise
    }

    И Импортировать его в TrackDetail.tsx:

    TrackDetail.tsx
    import { getTrack, GetTrackDetailsOutputData } from "../dal/api.ts"
    //...
    📌

    Типы, описывающие данные, полученные от сервера, должны находиться в том же модуле, где впервые используются. Это логично, потому что именно DAL первым «касается» данных сервера.

    Рефакторинг TracksList

    Реализация функции getTracks

    api.ts
    // ...
     
    export const getTracks = () => {
      const promise = fetch("https://musicfun.it-incubator.app/api/1.0/playlists/tracks/", {
        headers: {
          "api-key": "28ea...", // ❗Подставь свой API-KEY
        },
      }).then((res) => res.json())
      return promise
    }

    Использование в компоненте

    TrackList.tsx
    //...
    useEffect(() => {
      //...
      getTracks().then((json) => setTracks(json.data))
    }, [])
     
    //...

    Типизация

    • Перенесем все необходимые типы в DAL
    api.ts
    //...
     
    export type AttachmentDto = {
      url: string
    }
     
    export type TrackListItemOutputAttributes = {
      title: string
      attachments: Array<AttachmentDto>
    }
     
    export type TrackListItemOutput = {
      id: string
      attributes: TrackListItemOutputAttributes
    }
     
    //...
    • Избавимся от инлайновых типов ({data: GetTrackDetailsOutputData}):
    api.ts
    //...
     
    export type GetTrackDetailsOutput = { data: GetTrackDetailsOutputData }
     
    export const getTrack = (trackId: string) => {
      const promise: Promise<GetTrackDetailsOutput> = //{...}
    }
     
    //...
    export type GetTrackListOutput = {
      data: Array<TrackListItemOutput>
    }
     
    export const getTracks = () => {
      const promise: Promise<GetTrackDetailsOutput> = //{...}
    }

    Mocking данных

    Выделение DAL позволяет легко подменить реализацию функции, сохраняя ее интерфейс(например: вход — trackID, выход — Promise с данными).

    • Можно создать фейковый API-модуль, который не выполняет реальных запросов, а возвращает заранее подготовленные данные, используя фабричную функцию Promise.resolve().
    • Такой подход позволяет разрабатывать и верстать приложение без обращения к реальному серверу и без расхода API-токенов.
    • Переключение между реальным и фейковым DAL выполняется просто — достаточно, например, переименовать файлы или изменить путь импорта.
    • Это облегчает тестирование, автономную разработку и демонстрацию интерфейса без зависимости от бэкенда.

    Пример: имитация запроса трека по ID

    Создадим api-fake.ts и в нем реализуем функцию getTrack(trackId: string), которая не делает запрос на сервер, а сразу возвращает Promise, разрешённый объектом с данными трека через Promise.resolve(). Типы импортируем из реального api.ts. Данные для Promise.resolve() возьмем из Network:

    api-fake.ts
    //...
     
    export const getTrack = (trackId: string) => {
      const promise = Promise.resolve({
        data: {
          id: "88133ec1-f82d-4fbb-b53b-5138b6fc7b90",
          type: "tracks",
          attributes: {
            title: "gangsta react",
            lyrics: "u are the winner, u love react, u'll get the offer, it's fact",
            user: {
              id: "19",
              name: "dimych",
            },
            releaseDate: "2025-09-05T13:00:15.155Z",
            addedAt: "2025-08-13T19:15:17.182Z",
            updatedAt: "2025-10-10T07:30:56.145Z",
            duration: 0,
            attachments: [
              {
                id: "b217ec35-9021-4318-b0ca-7b31df1958a6",
                addedAt: "2025-08-13T19:15:15.487Z",
                updatedAt: "2025-08-13T19:15:15.487Z",
                version: 1,
                url: "https://production-it-incubator.s3.eu-central-1.amazonaws.com/apihub-spotifun/Video/0ae77ecd-f271-4e4c-881c-74e4bdfb4046_file4.mp3",
                contentType: "audio/mpeg",
                originalName: "file4.mp3",
                fileSize: 16800,
              },
            ],
            images: {
              main: [
                {
                  type: "original",
                  width: 200,
                  height: 200,
                  fileSize: 14040,
                  url: "https://production-it-incubator.s3.eu-central-1.amazonaws.com/apihub-spotifun/Image/e46c0be9-f36e-4131-abbc-0e6512985550_cover.png",
                },
                {
                  type: "medium",
                  width: 156,
                  height: 156,
                  fileSize: 5615,
                  url: "https://production-it-incubator.s3.eu-central-1.amazonaws.com/apihub-spotifun/Image/f9340e41-b46e-4db2-a504-b37383e9c0bd_image.png",
                },
                {
                  type: "thumbnail",
                  width: 48,
                  height: 48,
                  fileSize: 945,
                  url: "https://production-it-incubator.s3.eu-central-1.amazonaws.com/apihub-spotifun/Image/b3bd1da4-7b4c-4fa9-a959-0915c3c4abf4_image.png",
                },
              ],
            },
            tags: [],
            artists: [],
            likesCount: 3,
            dislikesCount: 2,
            currentUserReaction: 0,
            publishedAt: "2025-08-13T19:16:27.070Z",
            isPublished: true,
          },
        },
      })
      return promise
    }
     
    //...

    По аналогии с getTrack(), можно реализовать и имитацию получения списка треков — getTracks(). Таким образом, весь DAL можно быстро "подменить" фейковой реализацией, что позволяет разрабатывать и тестировать UI и управление состоянием без обращения к настоящему API.

    Заключение 🚀

    ✔️ Правильное разделение ответственности, выделение DAL делают фронтенд-проект более структурированным, поддерживаемым и безопасным для изменений.

    ✔️ Компоненты сосредоточены на UI, DAL управляет данными, границы ответственности чётко определены — это основа профессиональной архитектуры React-приложений.


    🏠 Домашнее задание

    Цель задания: Разделить приложение на слои: ui, dal

    По аналогии как в видео необходимо доработать основное приложение trelly, над которым мы закончили работать в 21 домашнем задании

    Задание 1

    • Переименуй директорию components в ui.
    • Создай директорию dal на одном уровне с ui.
    • В dal директории создай файл api.ts
    • В файле api.ts создай функцию getTask и вынеси в нее код связанный с запросом на сервер из компонента TaskDetails.tsx
    dal/api.ts
    export const getTask = (/*...*/) => {
      /*...*/
    }

    Итоговый результат 🚀 разделили код на 2 слоя. Приложение работает как и прежде

    Задание 2

    • перенеси типизацию ответа с сервера в api.ts
    • протипизируй результат которым резолвится Promise для функции getTask чтобы при обращение в компоненте не было any
    • избавься от инлайновых типов и создай type GetTaskOutput согласно swagger документации

    Итоговый результат 🚀. Типизация перенесена, приложение корректно отрабатывает, есть подсказки от TS

    • до 😥

    22-1.before

    • после 🙂

    22-1.after

    Задание 3

    По аналогии с заданиями 1 и 2 сделай следующее:

    • в файле api.ts создай функцию getTasks и вынеси в нее код связанный с запросом на сервер из компонента TasksList.tsx
    • перенеси типизацию ответа с сервера в api.ts из TaskItem.tsx
    • протипизируй результат которым резолвится Promise для функции getTasks чтобы при обращение в компоненте не было any
    • избавься от инлайновых типов и создай type GlobalTaskListResponse согласно swagger документации

    Итоговый результат 🚀. Вынесли детали работы с сервером в api.ts. При этом приложение корректно отрабатывает, есть подсказки от TS

    Задание 4 ⭐

    ⭐ Дополнительное задание со звёздочкой. Проделывай по желанию.

    В последующих домашках моковые данные использоваться не будут. Это практика для закрепления и более глубокого понимания материла

    Шаги которые нужно выполнить:

    • в директории dal создай фейковую апишку api-fake.ts
    • скопируй полностью (вместе с типами) код из api.ts
    • вместо реальных запросов на сервер, возвращай Promise, который резолвится моковыми данными. Пример данных ты можешь достать из network или взять данные которые мы прикрепили
    dal/api-fake.ts
    // 🔶 Mock данные для получения одной таски (getTask)
    const task = {
      data: {
        id: "4f310604-82b5-4afd-b9a4-ddf12dfac0a3",
        type: "tasks",
        attributes: {
          title: "learn useEffect",
          order: -1,
          deadline: "2029-12-27T17:51:48.031Z",
          startDate: "2025-09-09T17:51:48.031Z",
          addedAt: "2025-09-09T08:30:59.034Z",
          priority: 3,
          status: 2,
          updatedAt: "2025-09-19T14:24:20.399Z",
          boardId: "13923117-72de-4788-a7f0-4c42f162a5ab",
          boardTitle: "hfgh",
          description:
            "useEffect is a React Hook that lets you synchronize a component with an external system.",
          attachments: [],
        },
      },
    }
     
    // 🔶 Mock данные для получения множества тасок (getTasks)
    const tasks = [
      {
        id: "4f310604-82b5-4afd-b9a4-ddf12dfac0a3",
        type: "tasks",
        attributes: {
          title: "learn useEffect",
          boardId: "13923117-72de-4788-a7f0-4c42f162a5ab",
          status: 2,
          priority: 3,
          addedAt: "2025-09-09T08:30:59.034Z",
          attachmentsCount: 0,
        },
      },
      {
        id: "07b51554-f680-4b5f-8e81-dbcbe32d08cc",
        type: "tasks",
        attributes: {
          title: "html",
          boardId: "e11c9480-dd73-4b08-a5fd-452465467805",
          status: 0,
          priority: 1,
          addedAt: "2025-08-27T17:51:48.031Z",
          attachmentsCount: 0,
        },
      },
      {
        id: "b6213cee-b407-4580-9276-be4f5919375d",
        type: "tasks",
        attributes: {
          title: "css",
          boardId: "e11c9480-dd73-4b08-a5fd-452465467805",
          status: 0,
          priority: 1,
          addedAt: "2025-08-27T17:51:44.710Z",
          attachmentsCount: 0,
        },
      },
      {
        id: "4c37b109-d930-4ad4-9e37-4f94d618b59a",
        type: "tasks",
        attributes: {
          title: "js",
          boardId: "e11c9480-dd73-4b08-a5fd-452465467805",
          status: 0,
          priority: 1,
          addedAt: "2025-08-27T17:51:21.515Z",
          attachmentsCount: 0,
        },
      },
      {
        id: "0319fde0-3e69-4240-9ee4-278ce525f7f6",
        type: "tasks",
        attributes: {
          title: "title3",
          boardId: "e11c9480-dd73-4b08-a5fd-452465467805",
          status: 0,
          priority: 0,
          addedAt: "2025-07-03T14:56:48.867Z",
          attachmentsCount: 0,
        },
      },
    ]
    • в компонентах TaskDetails.tsx и TasksList.tsx поправь импорты, чтобы доставать фейковые данные
    TaskDetails.tsx
    import { getTask, type TaskDetailsData } from "../dal/api-fake.ts"
    TasksList.tsx
    import { getTasks, type Task } from "../dal/api-fake.ts"

    Итоговый результат 🚀. В результате ты увидишь на экране теже таски, но запрос на сервер уходить не будет. И при нажатии на разные таски будет подгружаться всегда одна и таже таска. Такой подход позволяет разрабатывать и верстать приложение без обращения к реальному серверу и без расхода API-токенов.


    💡

    🔶 Помни: DAL — это не усложнение, а упрощение будущей жизни. Когда API меняется (а оно изменится), ты правишь код в одном месте, а не ищешь fetch по всему проекту. Сегодняшние 10 минут на создание DAL экономят завтрашние часы рефакторинга.

    🔶 Думай как архитектор: разделяй и властвуй. Компонент не должен знать, откуда приходят данные — с сервера, из localStorage или mock-данных. DAL — это шлюз между UI и внешним миром. Меняй источник данных без единого изменения в компонентах. 🎯

    🔶 Профессионализм — в деталях. Новичок пишет fetch прямо в компоненте. Мидл выносит в отдельную функцию. Сеньор создаёт DAL с типизацией, обработкой ошибок и единой точкой конфигурации. Какой путь выбираешь ты? 🚀⚡

    Боевой маршрут (React Путь Самурая: без альтернатив)

    Видеоурок - 24 видео из 30