📱 Подписаться
IT и цифровая трансформация

Handbook frontend. Луковая архитектура. Часть 2

📰 Habr 👁️ 0 просмотров

T1_IT33 минуты назадОбъяснить с

Handbook frontend. Луковая архитектура. Часть 2

Уровень сложностиСложныйВремя на прочтение31 минОхват и читатели1.1KБлог компании ИТ-холдинг Т1Анализ и проектирование систем*Проектирование и рефакторинг*Управление разработкой*ТуториалВ первой части мы рассмотрели теорию, лежащую в основе концепции луковой архитектуры. Теперь предлагаю рассмотреть практическое применение. Будет МНОГО кода.

Практика

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

• Слой 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).
• Поддержка опциональных механик: плагины и другие слои подключаются без изменения ядра, достаточно реализовать нужный интерфейс.Единственный недостаток — более высокий порог входа для новых разработчиков; чтобы эффективно работать, нужно понимать, как устроены слои, почему зависимости направлены внутрь и как адаптеры взаимодействуют с ядром через контракты. Но это плата за порядок и предсказуемость, которая окупается на длинной дистанции.

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

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

• Роберт Мартин. «Чистая архитектура»
• Эрих Гамма, Ричард Хелм, Роберт Джонсон, Джон Влиссидес. “Паттерны объектно‑ориентированного проектирования” (GoF)
• SOLID: https://ru.wikipedia.org/wiki/SOLID_(программирование) (подробный разбор — в книге «Чистая архитектура»)
• MVC: https://ru.wikipedia.org/wiki/Model‑View‑Controller
• CQRS: https://ru.wikipedia.org/wiki/CQRS
• Cohesion и Coupling: https://en.wikipedia.org/wiki/Cohesion_(computer_science), https://en.wikipedia.org/wiki/Coupling_(computer_programming)Паттерны, которые используем в слоях

• Facade: https://ru.wikipedia.org/wiki/Фасад_(шаблон_проектирования)• State: https://ru.wikipedia.org/wiki/Состояние_(шаблон_проектирования)
• DTO: https://ru.wikipedia.org/wiki/DTO
• Repository: https://martinfowler.com/eaaCatalog/repository.html
• Mapper (Data Mapper): https://martinfowler.com/eaaCatalog/mapper.html, https://en.wikipedia.org/wiki/Data_mapper_patternТеги:• архитектура
• луковая архитектураХабы:• Блог компании ИТ-холдинг Т1
• Анализ и проектирование систем
• Проектирование и рефакторинг
• Управление разработкой

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал