В первой части мы рассмотрели теорию, лежащую в основе концепции луковой архитектуры. Теперь предлагаю рассмотреть практическое применение. Будет МНОГО кода.

Практика

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

  • Слой ui может зависеть только от core (через интерфейсы) и bootstrap.

  • Слой core не может зависеть ни от кого, кроме bootstrap.

  • Слой data может зависеть только от core (через интерфейсы), внешних API и bootstrap.

  • Слой bootstrap может зависеть от всех слоёв для их настройки.

Структура луковой архитектуры:

  • modules — список модулей с луковой архитектурой (core, data, ui, bootstrap);

  • pages — список страниц, где будут отображаться контейнеры (smart‑компоненты Angular).

Если в процессе разработки у вас получилось так, что одного слоя просто нет или он является пустым, то нужно оставлять папку пустой, добавляя в неё файл.gitignore, чтобы другие разработчики в вашей команде понимали, по каким «рельсам» и что вносить в будущем.

Структура всегда должна быть сохранена.

Описание структуры модуля
library-list/
    |-bootstrap
        |-constants
            |-core (папка опциональная)
                |-constants.ts
                |-index.ts
            |-data (папка опциональная)
                |-constants.ts
                |-index.ts
            |-ui   (папка опциональная)
                |-constants.ts
                |-index.ts
        |-configurations
            |-layers
                |-core
                    |-configure-core-providers.interface.ts
                    |-configure-core.provider.ts
                |-data
                    |-configure-data-providers.interface.ts
                    |-configure-data.provider.ts
                |-ui
                    |-configure-ui-providers.interface.ts
                    |-configure-ui.provider.ts
                |-configure-layers.ts
                |-configure-providers.ts
                |-index.ts
       |-index.ts
       |-library-list-module.interface.ts
       |-library-list.module.ts
    |-core
        |-book
            |-enums
                |-book-status.enum.ts
            |-interfaces
                |-book.interface.ts
            |-tokens
        |-library
            |-interfaces
                |-library-paginate.interface.ts
                |-library-repository.interface.ts
                |-library-state.interface.ts
                |-library-store.interface.ts
                |-library.interface.ts
            |-tokens
       |-ticket
            |-interfaces
                |-history-reading-book.interface.ts
                |-readers-ticket.interface.ts
                |-ticket-validity-period.interface.ts
            |-tokens
                |-*.token.ts
       |-library-facade.interface.ts
       |-library.facade.ts
    |-data
        |-book
           |-dto
               |-book-change-request.interface.ts
               |-book-response.interface.ts
           |-gql
               |-create-book.graphql.ts
               |-update-book.graphql.ts
               |-filter-book.graphql.ts
        |-library
            |-interfaces
                |-library-mapper.interface.ts
            |-mappers
                |-library-mapper.service.ts
            |-gql
                |-create-library.graphql.ts
                |-update-library.graphql.ts
                |-filter-library.graphql.ts
            |-library.store.ts
            |-library.repository.ts
        | -translate
            | -translate-resolver.ts
            | -translate-config.interface.ts
            | -translate-config.token.ts
     |-ui
        |-containers
            |-library-list
                |-interfaces (папка опциональна)
                    |-<название-папки-с-названием-сущности>
                    |-...
                |-mappers (папка опциональна)
                    |-<название-папки-с-названием-сущности>
                    |-...
                |-services (папка опциональна)
                    |-<название-папки-с-названием-сущности>
                    |-...
                |-library-list.component.html
                |-library-list.component.scss
                |-library-list.component.spec.ts
                |-library-list.component.ts
        |-components
            |-<название-компонента>
               |-<название-компонента>.component.html
               |-<название-компонента>.component.scss
               |-<название-компонента>.component.spec.ts
               |-<название-компонента>.component.ts

Слой bootstrap

Слой bootstrap отвечает за настройку и конфигурацию системы. Сюда выносят всё, что связано с инициализацией, подключением зависимостей и хранением настроек.

Когда все настройки собраны в одном месте, ими проще управлять. Это даёт единую точку входа для конфигурации: не нужно искать параметры по разным файлам и слоям, достаточно зайти в bootstrap и увидеть полную картину.

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

В bootstrap можно и нужно хранить:

  • конфигурации для сборки и запуска модулей;

  • «магические» числа, строки, стандартные значения;

  • настройки подключения к внешним сервисам;

  • DI‑контейнеры и внедрение зависимостей.

В bootstrap НЕ должно быть других:

  • объявлений enum для других слоёв;

  • типов и интерфейсов для переиспользования в других слоях;

  • реализаций функций и классов для других слоёв.

В папке bootstrap/configurations/constants находятся константы, которые нужны для слоёв core, data, ui

Основная идея заключается в разделении констант по логическим слоям. Например, если вы используете в слое core какую‑то константу (скажем, в паттерне «фасад»), которая лежит в data, то что‑то пошло не так.

Такое разделение констант также помогает понять, что должно относиться к конкретному слою, и корректнее спроектировать структуру и взаимодействие слоёв, не нарушая чистую архитектуру.

|-bootstrap
    |-constants
        |-core (папка опциональная)
            |-constants.ts
            |-index.ts
        |-data (папка опциональная)
            |-constants.ts
            |-index.ts
        |-ui   (папка опциональная)
            |-constants.ts
            |-index.ts
    |-...

Файл constants/ui/constants.ts:

export const UI_MESSAGES = {
    changeSubmit: 'Вы уверены, что хотите сохранить данные',
    deleteSubmit: 'Вы уверены, что хотите удалить запись',
};

export const CURRENT_BOOK_DEFAULT = {
    book: {},
    user: {},
};

export const SEARCH_DEFAULT = '';

Файл constants/core/constants.ts:

export const CORE_MESSAGES = {
    successfullyChange: 'Запись успешно изменена',
    successfullyDelete: 'Запись успешно удалена',
    error: 'Произошла ошибка: ',
    methodError: 'Method not implemented.'
};

Файл constants/data/constants.ts:

import { ILibraryState } from '../../../core/library';

export const LIBRARY_STATE_DEFAULT: ILibraryState = {
    list: [],
    searchBook: '',
    pagination: {
        currentPage: 1,
        selectedSize: 5,
        pageSizes: [5, 10, 20, 50]
    },
    loading: true,
    sortColumn: {
        type: 'asc | desc',
        name: ''
    },
    message: '',
};

export const REQUEST_PARAMS = {
    paginate: 'paginate',
    author: 'author'
};

export const DATA_MESSAGES = {
    methodError: 'Method not implemented.'
};

В папке bootstrap/configurations/layers находятся конфигурация для слоёв core, data и ui

Папка layers это точка сборки архитектурных слоев приложения. Здесь каждый слой (core, data, ui) предоставляет свой контракт как набор возможностей, которые он поддерживает. А конкретная реализация этих возможностей определяется внешними провайдерами.

Таким образом достигается:

  • гибкость: можно легко подменить реализацию (например, для тестов);

  • явность: чётко видно, какие настройки доступны для каждого слоя;

  • независимость: слои не знают друг о друге, их конфигурация изолирована.

|-bootstrap
    |-...
    |-configurations
        |-layers
            |-core
                |-configure-core-providers.interface.ts
                |-configure-core.provider.ts
            |-data
                |-configure-data-providers.interface.ts
                |-configure-data.provider.ts
            |-ui
                |-configure-ui-providers.interface.ts
                |-configure-ui.provider.ts
            |-configure-layers.ts
            |-configure-providers.ts
            |-index.ts

Папка layers/ui

Файл layers/ui/configure‑ui‑providers.interface.ts:

import { Type } from '@angular/core';
import { Observable } from 'rxjs';

/**
 * Конфигурация провайдеров для ui слоя
 * Используется для настройки зависимостей и передачи данных между слоями
 */
export interface IUiProvidersConfiguration {
  /**
   * Фабрика для получения идентификатора книги
   * 
   * Предоставляет способ передачи bookId из внешнего контекста (например, из роута или родительского компонента)
   * в ui слой модуля через Observable. Это позволяет реактивно отслеживать изменения bookId.
   */
  bookIdFactory?: Type<Observable<number>>;
}

Файл layers/ui/configure‑ui.provider.ts:

import { EnvironmentProviders, Provider } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { IUiProvidersConfiguration } from './configure-ui-providers.interface';
import { BOOK_ID, bookIdFactory } from '../../../../ui/containers/providers/book-id.providers';

/**
 * Настраивает провайдеры для ui слоя модуля.
 * 
 * Позволяет гибко конфигурировать зависимости, передаваемые в ui,
 * с возможностью переопределения стандартных фабрик через configuration.
 * 
 * @param configuration - опциональная конфигурация для переопределения провайдеров
 * @returns массив провайдеров для регистрации в модуле
 */
export const configureUiProviders = (
  configuration?: IUiProvidersConfiguration
): (Provider | EnvironmentProviders)[] => {
  return [
    {
      provide: BOOK_ID,
      deps: [ActivatedRoute],
      useFactory: configuration?.bookIdFactory ?? bookIdFactory,
    },
  ];
}; 

Папка layers/core

Файл layers/core/configure‑core‑providers.interface.ts:

import { Type } from '@angular/core';
import { ILibraryFacade } from '../../../../core/library-facade.interface';

/**
 * Конфигурация провайдеров для core слоя
 * Позволяет переопределять реализации фасада и других зависимостей ядра
 */
export interface ICoreProvidersConfiguration {
  /**
   * Фасад для работы с библиотекой
   * 
   * Предоставляет возможность подставить альтернативную реализацию фасада,
   * например, для тестирования, мокирования или расширения функциональности.
   */
  libraryFacade?: Type<ILibraryFacade>;
}

Файл layers/core/configure‑core.provider.ts:

import { EnvironmentProviders, Provider } from '@angular/core';

import { ICoreProvidersConfiguration } from './configure-core-providers.interface';
import { LIBRARY_FACADE } from '../../../../core/library';
import { LibraryFacade } from '../../../../core/library.facade';

/**
 * Настраивает провайдеры для core слоя модуля.
 *
 * Позволяет гибко конфигурировать зависимости ядра с возможностью переопределения
 * реализации фасада через конфигурацию. Это даёт возможность подменять реализации
 * для тестирования, мокирования или расширения функциональности.
 *
 * @param configuration - опциональная конфигурация для переопределения провайдеров
 * @returns массив провайдеров для регистрации в модуле
 */
export const configureCoreProviders = (
  configuration?: ICoreProvidersConfiguration
): (Provider | EnvironmentProviders)[] => {
  return [
    {
      provide: LIBRARY_FACADE,
      useClass: configuration?.libraryFacade ?? LibraryFacade,
    },
  ];
};

Папка layers/data

Файл layers/data/configure‑data‑providers.interface.ts:

Код
import { Type } from '@angular/core';

import { ILibraryRepository, ILibraryStore } from '../../../../core/library';
import { ILibraryMapper } from '../../../../data/library/mappers/library-mapper.interface';
import { IApiUrl } from '../../../../data/api-url.interface';

/**
 * Конфигурация провайдеров для data слоя
 * Позволяет переопределять реализации репозитория, маппера, стора и URL для API
 */
export interface IDataProvidersConfiguration {
  /**
   * Репозиторий для работы с данными библиотеки
   * 
   * Отвечает за выполнение запросов к API и получение данных.
   * Позволяет подменить стандартную реализацию для тестирования,
   * мокирования или добавления дополнительной логики.
   */
  libraryRepository?: Type<ILibraryRepository>;
  /**
   * Маппер для преобразования данных между структурами
   * 
   * Сопоставляет данные между внешним форматом (бэкенд/API) и внутренней доменной моделью.
   * Может быть переопределён для поддержки разных версий API или структур данных.
   */
  libraryMapper?: Type<ILibraryMapper>;
  /**
   * Хранилище состояния для библиотеки
   * 
   * Централизованное хранилище данных модуля. Позволяет подменить реализацию
   * для тестирования или использования альтернативных стратегий управления состоянием.
   */
  libraryStore?: Type<ILibraryStore>;
  /**
   * URL для API запросов
   * 
   * Конфигурация базового URL и эндпоинтов для работы с бэкендом.
   * Позволяет переключаться между окружениями (dev, staging, prod)
   * или подставлять разные адреса
   */
  apiUrl?: IApiUrl;
}

Файл layers/data/configure‑data.provider.ts:

Код
import { EnvironmentProviders, Provider } from '@angular/core';

import { IDataProvidersConfiguration } from './configure-data-providers.interface';
import { LIBRARY_REPOSITORY, LIBRARY_STORE } from '../../../../core/library';
import { LibraryRepository } from '../../../../data/library/library.repository';
import { LibraryStore } from '../../../../data/library/library.store';
import { LibraryMapper } from '../../../../data/library/mappers/library-mapper.service';
import { LIBRARY_MAPPER } from '../../../../data/library/tokens/library-mapper-token';
import { API_URL } from '../../../../data/api-url';

/**
 * Настраивает провайдеры для data слоя модуля.
 *
 * Регистрирует все зависимости, связанные с работой с данными:
 * репозиторий для запросов к API, маппер для преобразования структур,
 * стор для хранения состояния и URL для подключения к бэкенду.
 *
 * Позволяет переопределять любую реализацию через конфигурацию,
 * что даёт гибкость для тестирования, мокирования или подмены реализаций.
 *
 * @param configuration - опциональная конфигурация для переопределения провайдеров
 * @returns массив провайдеров для регистрации в модуле
 */
export const configureDataProviders = (
  configuration?: IDataProvidersConfiguration
): (Provider | EnvironmentProviders)[] => {
  return [
    {
      provide: LIBRARY_REPOSITORY,
      useClass: configuration?.libraryRepository ?? LibraryRepository,
    },
    {
      provide: LIBRARY_MAPPER,
      useClass: configuration?.libraryMapper ?? LibraryMapper,
    },
    {
      provide: LIBRARY_STORE,
      useClass: configuration?.libraryStore ?? LibraryStore,
    },
    {
      provide: API_URL,
      useValue: configuration?.apiUrl ?? '',
    },
  ];
};

Файл configure‑layers.ts

Код
import { EnvironmentProviders, Provider } from '@angular/core';

import { configureCoreProviders } from './core';
import { configureDataProviders } from './data';
import { configureUiProviders } from './ui';
import { ILayersProviders } from '../library-list-module.interface';

/**
 * Конфигурирование слоёв луковой архитектуры модуля
 *
 * Основная точка входа для регистрации всех провайдеров модуля.
 * Собирает вместе конфигурации каждого слоя (core, data, ui),
 * позволяя гибко настраивать модуль через единый объект конфигурации.
 *
 * @param configuration - опциональная конфигурация для всех слоёв
 * @param configuration.core - конфигурация для core слоя
 * @param configuration.data - конфигурация для data слоя
 * @param configuration.ui - конфигурация для ui слоя
 * @returns массив провайдеров для регистрации в модуле
 *
 * @example
 * // Базовое использование без конфигурации
 * const providers = configureLayers();
 *
 * @example
 * // Конфигурирование всех слоёв
 * const providers = configureLayers({
 *   core: {
 *     libraryFacade: CustomLibraryFacade
 *   },
 *   data: {
 *     apiUrl: { baseUrl: 'https://api.example.com' }
 *   },
 *   ui: {
 *     bookIdFactory: () => this.route.params.pipe(map(p => p['id']))
 *   }
 * });
 *
 * @example
 * // Переопределение только одного слоя
 * const providers = configureLayers({
 *   data: {
 *     libraryRepository: MockLibraryRepository
 *   }
 * });
 */
export const configureLayers = (
  configuration?: ILayersProviders
): (Provider | EnvironmentProviders)[] => [
  ...configureCoreProviders(configuration?.core),
  ...configureDataProviders(configuration?.data),
  ...configureUiProviders(configuration?.ui),
];

Файл configure‑providers.ts

import { EnvironmentProviders, Provider } from '@angular/core';

import { configureLayers } from './configure-layers';
import { IConfigurationsProviders } from '../library-list-module.interface';

/**
 * Основная точка входа для конфигурации всех провайдеров модуля
 *
 * Собирает вместе:
 * 1. Провайдеры слоёв архитектуры (core, data, ui) через configureLayers
 * 2. Внешние провайдеры, переданные извне (например, для расширения функциональности)
 *
 * @param configuration - опциональная конфигурация
 * @param configuration.layers - конфигурация для слоёв архитектуры
 * @param configuration.externalProviders - дополнительные внешние провайдеры
 * @returns массив провайдеров для регистрации в модуле
 */
export const configureProviders = (
  configuration?: IConfigurationsProviders
): (Provider | EnvironmentProviders)[] => [
  ...configureLayers(configuration?.layers),
  ...configuration?.externalProviders ?? [],
];

Файл index.ts

export * from './core';
export * from './data';
export * from './ui';

export * from './configure-layers';
export * from './configure-providers';

В папке bootstrap, где находится сам модуль

Файл index.ts:

export * from './layers';

Файл library‑list‑module.interface.ts:

Код
import { EnvironmentProviders, Provider } from '@angular/core';

import {
    ICoreProvidersConfiguration,
    IDataProvidersConfiguration,
    IUiProvidersConfiguration
} from './layers';

/**
  * Конфигурация модуля LibraryList
  * Используется при инициализации модуля через forRoot()
  * 
  */
export interface ILibraryListModuleConfig {
    /**
     * Конфигурация слоев модуля
     * Позволяет переопределить стандартные реализации слоев:
     *     
     *     - data - конфигурация слоя данных (Store, Repository, Mapper  и тд)
     *     - core - конфигурация ядра модуля (сервисы, facade и тд)
     *     - ui - конфигурация UI компонентов (сервисы, handlers, компоненты и тд)
     * @example
     * ```typescript
     *        layers: {
     *            data: {
     *                libraryStore: CustomLibraryStore, // Свой Store
     *                libraryRepository: CustomLibraryRepository, // Свой Repository
     *                apiUrl: { // URL для API
     *                    list: 'v2/books/list',
     *                    change: 'v2/books/update',
     *                    delete: 'v2/books/remove'
     *                }
     *           }
     *       }
     * ```
    */
    layers?: ILayersProviders;
    /**
     * Внешние провайдеры для модуля
     * Позволяет добавить дополнительные провайдеры, которые будут доступны в модуле
     * @example
     * ```typescript
     *     externalProviders: [
     *         provideHttpClient(), // HTTP клиент
     *         provideRouter(routes), // Роутинг
     *         provideAnimations(), // Анимации
     *         { provide: APP_CONFIG, useValue: config } // Кастомные провайдеры
     *     ]
     */
    externalProviders?: (Provider | EnvironmentProviders)[],
}

export interface ILayersProviders {
    /**
     * Конфигурация слоя данных
     * Store, Repository, Mapper для работы с данными
     */
    data?: IDataProvidersConfiguration,
    /**
     * Конфигурация ядра модуля
     * Основные сервисы, фасады и тд
     */
    core?: ICoreProvidersConfiguration,
    /**
     * Конфигурация UI слоя
     * Компоненты, директивы, пайпы для отображения
    */
    ui?: IUiProvidersConfiguration
}

/**
 * Тип для полной конфигурации модуля
 * Объединяет все возможные конфигурации
 */
export interface IConfigurationsProviders extends ILibraryListModuleConfig {}

Файл library‑list.module.ts:

Код
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { FormsModule } from '@angular/forms';

import { LibraryListComponent } from '../../ui/containers/library-list.component';
import { configureProviders } from './layers';
import { ILibraryListModuleConfig } from './library-list-module.interface';

/**
 * Модуль для работы со списком книг библиотеки.
 *
 * Предоставляет компонент LibraryListComponent и все необходимые зависимости
 * для работы с данными по слоистой архитектуре.
 * Поддерживает настройку через forRoot и forChild.
 */
@NgModule({
  imports: [CommonModule, FormsModule],
  declarations: [LibraryListComponent],
  exports: [LibraryListComponent],
  providers: [provideHttpClient(withInterceptorsFromDi())],
})
export class LibraryListModule {

 /**
   * Подключение модуля в feature / lazy-модуле.
   *
   * Поведение совпадает с forRoot: те же configureProviders(config).
   * Отдельный метод нужен для читаемости и единого стиля Angular-модулей,
   * а не потому что провайдеры здесь «не глобальные».
  */
  public static forRoot(
    config?: ILibraryListModuleConfig
  ): ModuleWithProviders<LibraryListModule> {
    return {
      ngModule: LibraryListModule,
      providers: [configureProviders(config)],
    };
  }

  /**
   * Подключение модуля с конфигурацией для дочерних модулей
   *
   * Используется при ленивой загрузке или в фича-модулях.
   * В отличие от forRoot, не регистрирует глобальные провайдеры повторно.
   * Позволяет передать конфигурацию для конкретного контекста использования.
   */
  public static forChild(
    config?: ILibraryListModuleConfig
  ): ModuleWithProviders<LibraryListModule> {
    return {
      ngModule: LibraryListModule,
      providers: [configureProviders(config)],
    };
  }
} 

Слой ui

Единственная обязанность слоя ui — отображение данных, полученных из core, и передача ему действий пользователя. Здесь может выполняться преобразование данных из формата core в формат, удобный для отображения.

Структура отображения:

|-ui
    |-containers
        |-library-list // Контейнерный компонент (Smart)
            |-providers (папка опциональна)
                |-<название-папки-с-названием-сущности>
            |-interfaces (папка опциональна)
                |-<название-папки-с-названием-сущности>
                |-...
            |-mappers (папка опциональна)
                |-<название-папки-с-названием-сущности>
                |-...
            |-services (папка опциональна)
                |-<название-папки-с-названием-сущности>
            |-...
            |-library-list.component.html
            |-library-list.component.scss
            |-library-list.component.spec.ts
            |-library-list.component.ts
   |-components // Dump компоненты (презентационные)
       |-<название-компонента> // Каждый компонент в своей папке
           |-<название-компонента>.component.html
           |-<название-компонента>.component.scss
           |-<название-компонента>.component.spec.ts
           |-<название-компонента>.component.ts

Правила именования и организации файлов и папок:

  • Внутри containers папки именуют по названию контейнерного компонента (например, library‑list, book‑details, user‑profile). Каждый контейнер соответствует определённой странице или крупному функциональному блоку.

  • Внутри components папки именуют по названию презентационного компонента (например, book‑card, search‑input, pagination‑controls). Каждый компонент решает одну узкую задачу и не знает о данных за пределами своих входных параметров.

  • Опциональные папки внутри контейнера (interfaces, mappers, services) именуют по названию сущности, с которой они работают. Например, interfaces/book, mappers/book, services/book‑api. Это позволяет держать связанные сущности рядом и не разбрасывать их по разным углам проекта.

  • Имена должны отражать тематическую область: вместо абстрактных components/utils или containers/common используйте конкретные названия: book‑card, search‑form, filter‑panel. Так новый разработчик сразу поймё, что искать и где.

  • Сохраняется единообразие: структура повторяется от модуля к модулю. Это снижает когнитивную нагрузку, разработчик знает, что, например, маппер для книги всегда лежит в ui/containers/library‑list/mappers/book/, а не в случайном месте.

Файл library‑list.component.ts:

Код
import { ChangeDetectionStrategy, Component, computed, effect, Inject } from '@angular/core';

import { ILibraryFacade } from '../../core/library-facade.interface';
import { ILibrary, ILibraryCurrentBook, LIBRARY_FACADE } from '../../core/library';
import { CURRENT_BOOK_DEFAULT, UI_MESSAGES, SEARCH_DEFAULT } from '../../bootstrap/constants/ui';

@Component({
	selector: 'app-library-list',
	templateUrl: './library-list.component.html',
	styleUrls: ['./library-list.component.scss'],
	changeDetection: ChangeDetectionStrategy.OnPush
})
export class LibraryListComponent {
	protected readonly vm = this.libraryFacade.vm;

	protected list = computed(() => this.vm().list);
	protected message = computed(() => this.vm().message);

	protected currentBook: ILibraryCurrentBook = CURRENT_BOOK_DEFAULT;
	protected search: string = SEARCH_DEFAULT;
	protected stateForm: string | null = null;

	constructor(
		@Inject(LIBRARY_FACADE) private readonly libraryFacade: ILibraryFacade
	) {
		effect(() => this.showMessage());
		effect(() => this.changeSearchParam());
		effect(() => this.changeBook());
	}

	private showMessage(): void {
		const message = this.message();
		if (message === '') {
			return;
		}

		alert(this.message());
	}

	private changeSearchParam(): void {
		this.search = this.vm().searchBook;
	}

	private changeBook(): void {
		this.stateForm = '';
		this.currentBook = CURRENT_BOOK_DEFAULT;
	}

	protected onSearch(value: string): void {
		this.libraryFacade.searchBook(value);
	}

	protected onChangePaginate(size: number): void {
		this.libraryFacade.changePaginate(1, size);
	}

	protected onChange(item: ILibrary): void {
		this.stateForm = 'change';
		this.currentBook = structuredClone(item);
	}

	protected onDelete(item: ILibrary): void {
		if (!confirm(UI_MESSAGES.deleteSubmit)) {
			return;
		}

		this.libraryFacade.deleteBook(item.book.id);
	}

	protected onSubmit(): void {
		if (!confirm(UI_MESSAGES.changeSubmit) || !this.currentBook.book.id) {
			return;
		}

		this.libraryFacade.changeBook(structuredClone(this.currentBook) as ILibrary);
	}
}

Файл library‑list.component.html:

Код
@let vmSignal = vm();

@if(vmSignal) {
    <div>
        Поиск по автору: <input type="text" [(ngModel)]="search" (change)="onSearch(search)">
    </div>
    <div style="display: inline-block;">
        <button class="options" *ngFor="let pageSize of vmSignal.pagination.pageSizes" (click)="onChangePaginate(pageSize)">
            {{pageSize}}
        </button>
    </div>
    <div style="display: inline-block;">
        <button>Добавить</button>
    </div>
    <table border="1">
        <thead>
            <th>№</th>
            <th>Название книги</th>
            <th>Автор</th>
            <th>Дата выхода</th>
            <th>Кто изменил запись</th>
            <th>Действия</th>
        </thead>
        <tbody>
            <tr *ngFor="let item of vmSignal.list">
                <td>{{item.book.id}}</td>
                <td>{{item.book.name}}</td>
                <td>{{item.book.author}}</td>
                <td>{{item.book.datePublication}}</td>
                <td>{{item.user.surname}} {{item.user.name}}</td>
                <td>
                    <button (click)="onChange(item)">
                        Изменить
                    </button>
                    <button (click)="onDelete(item)">
                        Удалить
                    </button>
                </td>
            </tr>
        </tbody>
    </table>
}

@if(vmSignal) {
    @if (vmSignal.loading) {
        <div>Загрузка...({{vmSignal.loading}})</div>
    }
    <div>{{vmSignal.searchBook}}</div>
    <div>{{vmSignal.pagination.selectedSize}}</div>
}

@if(stateForm === 'change') {
    <div>Форма для редактирования</div>
    <div class="card-body">
        <form #changeBook="ngForm" (submit)="onSubmit()">
            <div class="form-group">
                <input type="text" name="id" class="form-control" placeholder="" [(ngModel)]="currentBook.book.id" disabled>
            </div>
            <div class="form-group">
                <input type="text" name="name" class="form-control" placeholder="" [(ngModel)]="currentBook.book.name" required>
            </div>
            <div class="form-group">
                <input type="text" name="datePublication" class="form-control" placeholder="" [(ngModel)]="currentBook.book.datePublication" required>
            </div>
            <div class="form-group">
                <input type="text" name="surname" class="form-control" placeholder="" [(ngModel)]="currentBook.user.surname" required>
            </div>
            <div class="form-group">
                <input type="text" name="userName" class="form-control" placeholder="" [(ngModel)]="currentBook.user.name" required>
            </div>
            <button class="btn btn-primary" [disabled]="changeBook.invalid"> Изменить </button>
        </form>
    </div>
}

Слой сore

Этот слой — сердце бизнес‑логики модуля. Он занимает центральное место в архитектуре и служит связующим звеном между интерфейсом и данными. Его главная задача — оркестрация, он управляет потоком данных и делегирует работу со внешними источниками слою data через контракты (репозиторий, хранилище). Слой ui к core только обращается (через фасад), core от ui не зависит и сам не занимается ни отображением, ни хранением.

Ключевой принцип: изменения в ui или data не должны затрагивать core. Это значит, что вы можете переписать интерфейс или сменить способ хранения данных, и бизнес‑правила останутся нетронутыми. Именно здесь сосредоточена основная ценность приложения, его логика и алгоритмы.

Для этого в core используем три ключевых подхода:

  • Паттерн «Фасад» предоставляет простой публичный интерфейс для компонентов ui. Всё взаимодействие с core происходит через фасад, который скрывает внутреннюю сложность. Компоненты не знают о существовании хранилища, репозитория или других внутренних механизмов, они просто вызывают методы фасада.

  • Принцип CQRS (Command Query Responsibility Segregation) разделяет операции на команды (изменение данных) и запросы (чтение данных). Это позволяет оптимизировать каждую операцию отдельно и делает код более предсказуемым.

  • View Model (vm) — модель данных, подготовленная специально для отображения. Фасад предоставляет ui готовую модель, которую компоненты только показывают, не занимаясь преобразованиями. Это обеспечивает чёткое разделение между тем, как данные выглядят и как они обрабатываются.

Что можно хранить в core:

  • интерфейсы и типы, используемые в других слоях;

  • бизнес‑правила и алгоритмы;

  • логику работы с состоянием приложения;

  • контракты (интерфейсы) для репозиториев и хранилищ, слоя data.

Чего в core быть НЕ должно:

  • реализаций работы с API или базой данных;

  • Компонентов интерфейса и логики отображения;

  • специфичных для фронтенда деталей (роутинг, DOM‑манипуляции).

Благодаря такой организации слой core остаётся независимым от внешних факторов. Его можно тестировать изолированно, переиспользовать в разных проектах и модифицировать, не боясь сломать интерфейс или данные. Это и есть его главная ценность: он делает бизнес‑логику устойчивой и защищённой от изменений в смежных слоях.

Структура отображения:

|-core
    |-book
        |-enums
            |-book-status.enum.ts
        |-interfaces
            |-book.interface.ts
        |-tokens
            |-*.token.ts
    |-library
        |-interfaces
            |-library-paginate.interface.ts
            |-library-repository.interface.ts
            |-library-state.interface.ts
            |-library-store.interface.ts
            |-library.interface.ts
        |-tokens
            |-*.token.ts
    |-<название-сущности>
            |-interfaces
                |-<название-сущности-1>.interface.ts
                |-<название-сущности-2>.interface.ts
                |-<название-сущности-3>.interface.ts
            |-tokens
                |-*.token.ts
   |-library-facade.interface.ts
   |-library.facade.ts

Файл library‑facade.ts:

Код
import { computed, effect, Inject, Injectable } from '@angular/core';
import { Observable, forkJoin, map, of, switchMap, first } from 'rxjs';

import { ILibraryFacade } from './library-facade.interface';
import {
    ILibrary,
    ILibraryRepository,
    ILibraryStore,
    LIBRARY_REPOSITORY,
    LIBRARY_STORE
} from './library';
import { CORE_MESSAGES } from '../bootstrap/constants/core';

@Injectable()
export class LibraryFacade implements ILibraryFacade {
    public readonly vm = this.libraryStore.vm;

    private search = computed(() => this.vm().searchBook);
    private selectedSize = computed(() => this.vm().pagination.selectedSize);

    constructor(
        @Inject(LIBRARY_STORE) private readonly libraryStore: ILibraryStore,
        @Inject(LIBRARY_REPOSITORY) private readonly libraryRepository: ILibraryRepository
    ) {
        effect(() => this.updateBookList());
    }

    private updateBookList(): void {
        this.libraryRepository
            .getBooks(this.selectedSize(), this.search())
            .pipe(first())
            .subscribe((books) => {
                this.libraryStore.updateList(books);
            });
    }

    public changeBook(book: Partial<ILibrary>): void {
        this.libraryStore.updateStore({ loading: true });

        this.libraryRepository.changeBook(book)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.libraryStore.updateBook(book);
                    this.libraryStore.updateStore({
                        loading: false,
                        message: CORE_MESSAGES.successfullyDelete
                    });
                },
                error: (error) => {
                    this.libraryStore.updateStore({
                        loading: false,
                        message: CORE_MESSAGES.error + `${ error.message }`
                    });
                }
            });
    }

    public deleteBook(id: number): void {
        const selectedSize = this.libraryStore.storeValue.pagination.selectedSize;
        const searchBook = this.libraryStore.storeValue.searchBook;

        this.libraryRepository.deleteBook(id)
            .pipe(
                switchMap((data) => {
                    return forkJoin([
                        of(data),
                        this.libraryRepository.getBooks(selectedSize, searchBook)
                    ]).pipe(
                        map((response) => {
                            return this.libraryStore.updateList(response[1]);
                        })
                    )
                }),
                first()
            )
            .subscribe({
                next: () => {
                    this.libraryStore.updateStore({
                        message: CORE_MESSAGES.successfullyDelete
                    });
                },
                error: (error) => {
                    this.libraryStore.updateStore({
                        message: CORE_MESSAGES.error + `${ error.message }`
                    });
                }
            });
    }

    public changePaginate(currentPage: number, selectedSize: number): void {
        this.libraryStore.updatePagination(currentPage, selectedSize);
    }

    public searchBook(value: string): void {
        this.libraryStore.updateSearchBook(value);
    }
}

Файл library‑facade.interface.ts:

Код
import { Observable } from 'rxjs';
import { ILibrary, ILibraryState } from './library';
import { Signal } from '@angular/core';

/**
 * Фасад для работы с библиотекой
 *
 * Единая точка входа для ui слоя. Предоставляет методы для управления
 * данными и состояние для отображения. Скрывает внутреннюю реализацию
 * работы с хранилищем и репозиторием.
 */
export interface ILibraryFacade {
  /**
   * Содержит готовое состояние, которое компоненты только отображают.
   * Использует Signal для реактивного обновления.
   */
  readonly vm: Signal<ILibraryState>;
  /**
   * Альтернативный доступ к View Model через Observable
   * Опционально. Нужен для интеграции с RxJS-кодом или обратной совместимости.
   */
  readonly vm$?: Observable<ILibraryState>;

  /**
   * Изменение параметров пагинации
   *
   * @param currentPage - номер текущей страницы
   * @param selectedSize - количество элементов на странице
   */
  changePaginate(currentPage: number, selectedSize: number): void;
  /**
   * Поиск книги по строке
   *
   * @param value - поисковый запрос
   */
  searchBook(value: string): void;
  /**
   * Удаление книги по идентификатору
   *
   * @param id - ID книги
   */
  deleteBook(id: number): void;
  /**
   * Изменение данных книги
   *
   * Принимает частичный объект, так как при редактировании могут передаваться
   * не все поля.
   *
   * @param book - обновлённые данные книги
   */
  changeBook(book: Partial<ILibrary>): void;
}

Контракты и токены в слое core

В слое сore хранятся только контракты (интерфейсы) и токены для внедрения зависимостей. Сами реализации находятся в слое data.

Контракты (интерфейсы):

  • library‑repository.interface.ts описывает контракт для работы с данными. Определяет, какие методы должен реализовать репозиторий для получения, сохранения и удаления данных. Реализация репозитория находится в слое data.

  • library‑state.interface.ts описывает структуру состояния модуля. Определяет, какие данные хранятся и как они организованы. Используется как описание модели для хранилища.

  • library‑store.interface.ts описывает контракт для хранилища. Определяет методы управления состоянием: чтение, обновление, подписка на изменения. Реализация хранилища находится в слое data.

Токены:

  • library‑repository.token.ts — токен для внедрения реализации репозитория. Позволяет подменять реализацию через DI.

  • library‑store.token.ts — токен для внедрения реализации хранилища. Аналогично позволяет гибко управлять зависимостями.

  • library‑facade.token.ts — токен для внедрения фасада. Используется компонентами для получения экземпляра фасада через DI.

Зачем это нужно:

  • Разделение ответственности: слой core описывает, что должно быть сделано, но не знает, как. Реализация вынесена в слой data.

  • Гибкость: через DI можно подменить реализацию без изменения кода слоя core. Это упрощает тестирование (моки) и расширение функциональности.

  • Единая точка доступа: интерфейсы и токены лежат в слое core, и все слои ссылаются на них. Это исключает дублирование и путаницу.

  • Соблюдение архитектурных принципов: слой core не зависит от слоя data, слой data зависит от слоя core. Это делает слой core независимым и защищённым от изменений в реализации.

Файл library‑repository.interface.ts:

import { Observable } from 'rxjs';

import { ILibrary } from './library.interface';

export interface ILibraryRepository {
    /**
     * Получить список книг библиотеки
     */
    getBooks(paginate: number, author: string): Observable<ILibrary[]>;
    /**
     * Обновить состояние по книге
     */
    updateBook(): void;
    /**
     * Поиск информации по картотеке
     */
    bookFileSearch(): void;
    /**
     * Изменение книги
     */
    changeBook(book: Partial<ILibrary>): Observable<ILibrary[]>;
    /**
     * Удаление книги
     */
    deleteBook(id: number): Observable<ILibrary[]>;
}

Файл library‑state.ts:

Код
import { IPagination } from './library-paginate.interface';
import { ILibrary } from './library.interface';

export interface ILibraryState {
    /**
     * Список информации по книгам
     */
    list: ILibrary[];
    /**
     * Пагинация
     */
    pagination: IPagination;
    /**
     * Поиск по названию книги
     */
    searchBook: string;
    /**
     * Сортировка по колонке
     */
    sortColumn: {
        type: 'asc | desc',
        name: string
    };
    /**
     * Статус загрузки
     */
    loading: boolean;
    /**
     * Сообщение
     */
    message: string;
}

Файл library‑store.interface.ts:

Код
import { Observable } from 'rxjs';
import { Signal } from '@angular/core';

import { ILibraryState } from './library-state';
import { ILibrary } from './library.interface';


export interface ILibraryStore {
    readonly vm: Signal<ILibraryState>;
    readonly vm$: Observable<ILibraryState>;

    storeValue: ILibraryState;

    /**
     * Обновление пагинации
     * @param currentPage - какая страница выбрана
     * @param selectedSize - размер подгружаемого списка
     */
    updatePagination(currentPage: number, selectedSize: number): void;
    /**
     * Обновление состояния поиска книги
     * @param search - какую книгу ищем
     */
    updateSearchBook(search: string): void;
    /**
     * Обновление книги
     * @param book - обновленная книга
     */
    updateBook(book: Partial<ILibrary>): void;
    /**
     * Обновление списка книг
     * @param list - обновленный список книг
     */
    updateList(list: ILibrary[]): void;
    /**
     * Удаление книги
     * @param id - идентификатор удаляемой книги
     */
    deleteBook(id: number): void;

    /**
     * Обновление хранилища
     * @param payload 
     */
    updateStore(payload: Partial<ILibraryState>): void;
}

Файл library‑repository.token.ts:

import { InjectionToken } from '@angular/core';

import { ILibraryRepository } from '../interfaces/library-repository.interface';

export const LIBRARY_REPOSITORY = new InjectionToken<ILibraryRepository> (
    '[LIBRARY_REPOSITORY]: Хранилище, отвечающее за состояние библиотеки'
);

Файл library‑store.token.ts:

import { InjectionToken } from ‘@angular/core’;

import { ILibraryStore } from ‘…/interfaces/library-store.interface’;

export const LIBRARY_STORE = new InjectionToken ( 
  ‘[LIBRARY_STORE]: Store отвечающий за состояние библиотеки’ 
);

Слой Data

Он отвечает за хранение, получение и отправку данных. Это единственный слой в архитектуре, который знает о внешних источниках информации API, базах данных, локальном хранилище и других внешних системах.

Главная задача — обеспечить слой core данными и предоставить механизмы для их сохранения. Слой data реализует контракты (интерфейсы), объявленные в слое core, но при этом сам не знает, кто и как будет использовать эти данные. Это обеспечивает слабую связанность и возможность замены способа хранения без влияния на бизнес‑логику.

Ключевые принципы:

  • Data следует контрактам, определённым в слое core (репозиторий, хранилище).

  • Data не знает о существовании ui и ничего не знает об отображении данных.

  • Data преобразует данные из внешних форматов во внутренние структуры.

  • Data управляет состоянием модуля (через хранилище) и синхронизирует его с внешними источниками.

Что находится в data:

  • Репозиторий с реализацией методов для работы с данными (запросы к API, обработка ответов).

  • Хранилище с реализацией управления состоянием (хранение, обновление).

  • Мапперы с преобразованием структур данных между внешним форматом (бэкенд) и внутренней доменной моделью.

  • DTO — это объекты для передачи данных между слоями.

  • Сервисы для работы с API, HTTP‑клиенты.

Чего в слое data быть НЕ должно:

  • Бизнес‑логики и алгоритмов (это ответственность Core).

  • Компонентов интерфейса и логики отображения.

  • Зависимостей от реализаций слоя core (класс фасада и подобное). Слой data зависит только от контрактов (интерфейсов и токенов) слоя core и реализует их.

Cлой data реализует контракты слоя core и отвечает за работу с внешними источниками данных. Он не содержит бизнес‑логики и не знает об отображении, но предоставляет слою core всё необходимое для работы. Изменения в слое data не затрагивают core и ui, что делает систему гибкой и легко поддерживаемой.

Структура отображения:

|-data
    |-book
        |-dto
            |-book-change-request.interface.ts
            |-book-response.interface.ts
        |-gql
            |-create-book.graphql.ts
            |-update-book.graphql.ts
            |-filter-book.graphql.ts
    |-library
        |-interfaces
            |-library-mapper.interface.ts
        |-mappers
            |-library-mapper.service.ts
        |-gql
            |-create-library.graphql.ts
            |-update-library.graphql.ts
            |-filter-library.graphql.ts
        |-library.store.ts
        |-library.repository.ts
    |-translate
        | -translate-resolver.ts
        | -translate-config.interface.ts
        | -translate-config.token.ts

Файл library‑store.ts:

Код
import { Injectable, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';

import { ILibrary, ILibraryState, ILibraryStore } from '../../core/library';
import { LIBRARY_STATE_DEFAULT } from '../../bootstrap/constants/data';

@Injectable()
export class LibraryStore implements ILibraryStore {
    private state = signal<ILibraryState>(LIBRARY_STATE_DEFAULT);

    public readonly vm = this.state.asReadonly();
    public readonly vm$ = toObservable(this.vm);

    public get storeValue(): ILibraryState {
        return this.vm();
    }

    /**
     * Обновление состояния объекта пагинации
     * @param currentPage - выбранная страница
     * @param selectedSize - размер загружаемого списка
     */
    public updatePagination(currentPage: number, selectedSize: number): void {
        const pagination = {
            ...this.storeValue.pagination,
            currentPage,
            selectedSize
        };

        this.updateStore({
            pagination,
            loading: true
        });
    }

    /**
     * Обновление состояния свойства поиска книги
     * @param search - какую информацию ищем
     */
    public updateSearchBook(search: string): void {
        this.updateStore({
            searchBook: search,
            loading: true
        });
    }

    public updateList(list: ILibrary[]): void {
        this.updateStore({
            list,
            loading: false
        });
    }

    public updateBook(book: Partial<ILibrary>): void {
        const newList = this.storeValue.list.map((item) => {
            return item.book.id === book?.book?.id ? { ...item, ...book } : item;
        });

        this.updateStore({
            list: newList,
            loading: true
        });
    }

    public deleteBook(id: number): void {
        const newList = this.storeValue.list.filter((item) => {
            return item.book.id !== id;
        });

        this.updateStore({
            list: newList,
            loading: true
        });
    }

    /**
     * Обновить внутренний кеш состояния и отправить его из хранилища
     * @param payload - новое состояние
     */
    public updateStore(payload: Partial<ILibraryState>): void {
        this.state.update(state => ({
            ...state,
            ...payload
        }));
    }
}

Файл library.repository.ts:

Код
import { Inject, Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';

import { Observable, catchError, map, throwError } from 'rxjs';
import { ILibrary, ILibraryRepository } from '../../core/library';

import { ILibraryMapper } from './mappers/library-mapper.interface';
import { IBookResponse } from '../book/dto/book-response.interface';
import { IBookRequest } from '../book/dto/book-change-request.interface';
import { API_URL } from '../api-url.token';
import { DATA_MESSAGES, REQUEST_PARAMS } from '../../bootstrap/constants/data';
import { IApiUrl } from '../api-url.interface';
import { LIBRARY_MAPPER } from './library-mapper.token';


@Injectable()
export class LibraryRepository implements ILibraryRepository {
    constructor(
        @Inject(API_URL) private readonly apiUrl: IApiUrl,
        @Inject(LIBRARY_MAPPER) private readonly libraryMapper: ILibraryMapper,
        private readonly httpClient: HttpClient
    ) {}

    public updateBook(): void {
        throw new Error(DATA_MESSAGES.methodError);
    }

    public bookFileSearch(): void {
        throw new Error(DATA_MESSAGES.methodError);
    }

    public getBooks(paginate: number, author: string): Observable<ILibrary[]> {
        const url: string = this.apiUrl['list'];
        const requestParams: HttpParams = new HttpParams()
            .set(REQUEST_PARAMS.paginate, paginate)
            .set(REQUEST_PARAMS.author, author);

        return this.httpClient.get<IBookResponse[]>(url, { params: requestParams })
            .pipe(
                map(response => {
                    return this.libraryMapper.mapFromLibrary(response);
                })
            );
    }

    public changeBook(book: Partial<ILibrary>): Observable<ILibrary[]> {
        const url: string = this.apiUrl['change'];
        const body: IBookRequest = this.libraryMapper.mapToBookChangeDto(book);

        return this.httpClient.post<IBookResponse[]>(url, body)
            .pipe(
                map(response => {
                    return this.libraryMapper.mapFromLibrary(response);
                }),
                catchError(this.onError)
            );
    }

    public deleteBook(id: number): Observable<ILibrary[]> {
        const url: string = this.apiUrl['delete'];
        const body = {
            id: id
        };

        return this.httpClient.post<IBookResponse[]>(url, body)
            .pipe(
                map(response => {
                    return this.libraryMapper.mapFromLibrary(response);
                }),
                catchError(this.onError)
            );
    }

    private onError(error: HttpErrorResponse): Observable<never> {
        return throwError(() => error);
    }
}

Файл library‑mapper.service.ts:

import { Injectable } from '@angular/core';

import { ILibraryMapper } from './library-mapper.interface';
import { IBookResponse } from '../../book/dto/book-response.interface';
import { ILibrary } from '../../../core/library';
import { IBookRequest } from '../../book/dto/book-change-request.interface';


@Injectable()
export class LibraryMapper implements ILibraryMapper {
    public mapFromLibrary(response: IBookResponse[]): ILibrary[] {
        return response as ILibrary[];
    }

    public mapToBookChangeDto(book: Partial<ILibrary>): IBookRequest {
        return {
            bookId: book?.book?.id ?? 0,
            bookName: book?.book?.name ?? '',
            bookAuthor: book?.book?.author ?? '',
            bookDatePublication: book?.book?.datePublication ?? new Date()
        };
    } 
}

Как слои работают вместе

  • UI →: пользователь нажимает на кнопку «Изменить книгу» и вызывается метод onChangeBook($event) компонента Dump из сервиса LibraryListComponent.

  • UI → Core: из компонента Dump отправляется событие выше с данными в компонент Smart и вызывается метод changeBook(book) из сервиса LibraryFacade, и передаёт данные из слоя ui в core.

  • Core → Data: слой core вызывает метод changeBook(book) у сервиса LibraryRepository для сохранения данных на сервер, и передаёт управление слою data.

  • Data(Маппинг): слой data пересопоставляет структуры данных, которые приняты на фронтенде, со структурой, которая принята на бэкенде, и возвращает новую структуру данных обратно в сервис LibraryRepository.

  • Data → Backend: сервис LibraryRepository отправляет данные на сохранение бэкенду.

  • Backend → Data: через какое‑то время получает обновленные данные по обновлённой книге.

  • Data (обратный маппинг): пересопоставляем пришедшую с бэкенда во фронтенд структуру данных с доменной моделью.

  • Data → Core: передаём информацию обратно со слоя data в core.

  • Core → Data (обновление в Store): по изменённой книге обновляем данные в хранилище.

  • Data → Core: обновлённая модель данных (vm) автоматически отправляется из слоя data в core.

  • Core → UI: обновлённая модель данных (vm) автоматически отправляется из слоя core в ui в компонент Smart.

  • UI(Smart → Dump): компонент Smart в асинхронном режиме передаёт в компонент Dump новую модель данных (vm) для отрисовки.

Выводы

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

Что мы получили:

  • Слабая связанность слоёв: изменения во внешних адаптерах (data, ui) не тянут за собой переписывание ядра (core).

  • Высокая скорость внесения правок: правим нужный слой, не трогая остальные; ядро остаётся нетронутым при замене внешних технологий.

  • Лёгкое расширение функциональности: новую точечно добавляем в ядро или в другой слой.

  • Упрощается написание тестов и документации: каждый слой тестируем и описываем отдельно, ядро тестируем изолированно от внешних зависимостей.

  • Ускоренная проверка кода: понятно, к какому слою относится изменение, и кто за него отвечает.

  • Параллельная разработка: несколько разработчиков могут работать в одном модуле, не мешая друг другу, один пишет core, другой — data, третий — ui.

  • Чёткое разделение ответственности: ядро отвечает за бизнес‑правила, адаптеры — за взаимодействие с внешним миром.

  • Проблемы локализуются быстрее: сразу понятно, в каком слое искать причину: если сломалась логика — виноват core, если API — data, если вёрстка — ui.

  • Упрощается навигация по проекту: структура предсказуема и интуитивно понятна: все знают, что core не зависит от HTTP и БД.

  • Быстрая масштабируемость: добавляются новые модули, а не переписываются старые; внешние адаптеры можно заменять без правок в ядре.

  • Конфигурируемость: возможность настраивать модули при передаче между проектами через сборку (bootstrap).

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

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

Полезная литература

Архитектура и принципы

Паттерны, которые используем в слоях