В данной архитектуре сложная бизнес-логика, не связанная напрямую с отображением, выносится в отдельный слой – слой доменной логики (Domain Layer) или слой сценариев/Use Case. При использовании подхода Feature-Driven Development (FSD) можно организовать это следующим образом:
-
Отдельная фича или доменный модуль
Для сущности Game можно создать фичу
game
, внутри которой будут:- Use Case / сценарии (например, сценарий добавления игры). В нем реализована цепочка действий: обход файлов, парсинг файла README.md, подготовка данных и вызов API для загрузки.
- Сервисы/обработчики. Например, сервис для обработки файлов (поиск и парсинг README.md) и сервис для загрузки данных на сервер.
-
Интерфейсы и контракты
Внутри фичи определяются контракты (интерфейсы) для каждого сценария. Это позволяет легко подменять реализации (например, для тестирования или изменения бизнес-логики).
-
Оркестрация через use-case функции
Сценарий (use-case) инкапсулирует вызовы необходимых сервисов. Он последовательно выполняет все этапы (обход файлов, парсинг, загрузка) и может принимать callback-функцию для обновления прогресса. UI-компонент лишь инициирует сценарий и подписывается на обновления (например, отображение прогресса).
-
Взаимодействие с UI через абстракции
UI-компоненты отвечают за сбор входных данных, отображение прогресса и предоставление возможности выбора вариантов развития сценария. Они не содержат бизнес-логику, а лишь вызывают соответствующий сценарий (use-case), который реализует всю сложную асинхронную логику.
Представим, что структура проекта выглядит следующим образом:
-
features/game/components/GameUploader.vue
UI-компонент для выбора файлов и отображения прогресса загрузки. -
features/game/use-cases/addGame.ts
Сценарий добавления игры. Здесь описан flow, включающий:- Обход выбранных файлов и поиск файла README.md для парсинга.
- Парсинг найденного файла для извлечения нужных данных.
- Загрузку файлов и извлеченных данных на сервер.
-
features/game/services/FileProcessingService.ts
Сервис, отвечающий за обработку файлов: обход директорий, поиск и парсинг файла README.md. -
features/game/services/UploadService.ts
Сервис, реализующий загрузку данных (файлов и результатов парсинга) на сервер. -
features/game/types/index.ts
Файл с интерфейсами и типами, описывающими контракт сценариев и сервисов для работы с сущностью Game.
Также могут существовать другие фичи, такие как screenshots
или publisher
, каждая из которых реализует свои сценарии и сервисы. При наличии пересечений между сущностями (например, связь между Game и Screenshots) можно организовать общий слой для обработки подобных взаимодействий.
-
Доменная логика (use-case слой)
Логика обработки сложных асинхронных сценариев (например, добавление игры с обходом файлов, парсингом и загрузкой) инкапсулируется в use-case функции, которые располагаются внутри соответствующей фичи (features/game/use-cases/addGame.ts
). -
Сервисы
Детали работы с файловой системой, API и т.п. находятся в сервисах (например,FileProcessingService
иUploadService
). Эти сервисы не зависят от UI и легко тестируются независимо. -
UI-компоненты
UI отвечает за предоставление интерфейса для выбора файлов и отображения прогресса. Они инициируют выполнение use-case функций и реагируют на их результаты (например, обновляя прогресс или показывая сообщение об успехе/ошибке).
Таким образом, при использовании FSD:
- Доменная логика реализуется в отдельном слое (use-case + сервисы) и не зависит от UI.
- UI лишь инициирует процесс и отображает его состояние, оставаясь независимым от бизнес-логики.
- В дальнейшем, при добавлении новых сущностей (например, скриншотов или издателей) или новых сценариев с пересечением между сущностями, можно расширять соответствующие фичи и общий доменный слой, сохраняя чистоту архитектуры.
Предположим, структура проекта выглядит так:
src/
├─ features/
│ ├─ game/
│ │ ├─ components/
│ │ │ └─ GameUploader.vue // UI-компонент для выбора файлов, отображения прогресса
│ │ ├─ use-cases/
│ │ │ └─ addGame.ts // Сценарий добавления игры
│ │ ├─ services/
│ │ │ ├─ FileProcessingService.ts
│ │ │ └─ UploadService.ts
│ │ └─ types/
│ │ └─ index.ts // Интерфейсы и типы, описывающие контракт сценариев
│ ├─ screenshots/ ... // Фича для скриншотов
│ └─ publisher/ ... // Фича для издателей
├─ shared/ // Общие утилиты, например, для работы с файлами, API-клиенты и т.п.
└─ main.ts
Пример use-case: addGame.ts
// src/features/game/use-cases/addGame.ts
import { FileProcessingService } from '../services/FileProcessingService';
import { UploadService } from '../services/UploadService';
export interface AddGameResult {
success: boolean;
parsedData?: any;
error?: string;
}
/**
* Функция, реализующая flow добавления игры.
* Принимает список путей к выбранным файлам/папкам и опционально callback для обновления прогресса.
*/
export async function addGame(
filePaths: string[],
onProgress?: (progress: number) => void
): Promise<AddGameResult> {
try {
// 1. Проходимся по файлам и ищем README.md
const parsedData = await FileProcessingService.parseFilesForReadme(filePaths, onProgress);
// 2. Начинаем загрузку файлов и данных
const uploadResult = await UploadService.uploadGameData(filePaths, parsedData, onProgress);
return { success: uploadResult };
} catch (error: any) {
return { success: false, error: error.message || 'Ошибка при добавлении игры' };
}
}
// src/features/game/services/FileProcessingService.ts
export class FileProcessingService {
static async parseFilesForReadme(
filePaths: string[],
onProgress?: (progress: number) => void
): Promise<any> {
// Логика обхода выбранных путей, поиска файла README.md, парсинг его содержимого
// При необходимости вызываем onProgress(percentage)
console.log("Начинается парсинг файлов:", filePaths);
// Имитация асинхронной работы:
await new Promise((resolve) => setTimeout(resolve, 1000));
// Возвращаем некие распарсенные данные
return { title: "Название игры", description: "Описание игры" };
}
}
// src/features/game/services/UploadService.ts
export class UploadService {
static async uploadGameData(
filePaths: string[],
parsedData: any,
onProgress?: (progress: number) => void
): Promise<boolean> {
// Логика загрузки файлов и данных на сервер
console.log("Начинается загрузка данных игры:", parsedData);
// Имитация асинхронной загрузки
await new Promise((resolve) => setTimeout(resolve, 2000));
return true;
}
}
<script lang="ts" setup>
// src/features/game/components/GameUploader.vue
import { ref } from 'vue';
import { addGame } from '../use-cases/addGame';
const selectedFiles = ref<string[]>([]);
const progress = ref(0);
const message = ref('');
async function onUpload() {
message.value = 'Запуск загрузки...';
const result = await addGame(selectedFiles.value, (p) => {
progress.value = p;
});
message.value = result.success ? 'Игра успешно добавлена' : `Ошибка: ${result.error}`;
}
</script>
<template>
<div>
<!-- UI для выбора файлов, здесь условно через input[type=file] -->
<input type="file" multiple @change="(e) => { /* заполнить selectedFiles.value */ }" />
<button @click="onUpload">Добавить игру</button>
<p>Прогресс: {{ progress }}%</p>
<p>{{ message }}</p>
</div>
</template>