FE в контексті впровадження Clean Architecture
Перед тим як пояснити причини, чому ми вирішили впровадити Clean Architecture у FE, слід зрозуміти принципи SOLID.
Ось чому ми дотримуємося принципів SOLID:
Якщо дотримуватись принципів SOLID?
Покращення повторного використання коду
Розділення компонентів на менші частини дозволяє легко повторно використовувати їх у різних проектах.
Легкість підтримки
Чітке розподілення відповідальностей полегшує виправлення помилок та додавання нових функцій.
Легкість тестування
Малі компоненти або впровадження залежностей через ін'єкцію дозволяють легко проводити mock тестування.
Ефективність співпраці
Чітка структура коду робить його легшим для розуміння командою, а чітко визначені залежності між компонентами спрощують паралельну роботу.
Оптимізація продуктивності
Оскільки компоненти розділені на маленькі частини, зменшується ймовірність непотрібного ререндерингу.
Отже, як Clean Architecture застосовується з точки зору SOLID?
Принцип єдиного обов'язку
- Clean Architecture чітко розділяє шари на Entity, Usecase, Interface Adaptor, Framework.
- Кожен шар має свою чітко визначену відповідальність. Наприклад, шар Entity відповідає лише за бізнес-правила, а шар Interface Adaptor лише за перетворення даних у зовнішні формати.
Принцип відкритості/закритості
- Через правила залежностей внутрішні шари не залежать від змін зовнішніх.
- При додаванні нової функції не потрібно змінювати старий код, а можна додавати нові реалізації.
Принцип заміщення Ліскофа
- Абстракція через інтерфейси дозволяє взаємозамінність реалізацій.
- Наприклад, заміна реалізації бази даних з MySQL на PostgreSQL не змінює бізнес-логіку.
Принцип розділення інтерфейсів
- Інтерфейси визначаються тільки на межах шарів.
- Наприклад, Repository Interface містить лише методи, необхідні для бізнес-логіки.
Принцип інверсії залежностей (Dependency Inversion Principle)
- Внутрішні шари (Entity, Usecase) не залежать від зовнішніх (база даних, UI).
- Усі залежності спрямовані всередину, що захищає внутрішню бізнес-логіку від змін зовнішніх компонентів.
// Шар Entity (найглибший)
export class User {
private readonly id: string;
private name: string;
constructor(id: string, name: string) {
this.id = id;
this.name = name;
}
// Методи, що містять бізнес-правила
public getName(): string {
return this.name;
}
public updateName(newName: string): void {
if (!newName || newName.trim().length === 0) {
throw new Error('Name cannot be empty');
}
this.name = newName;
}
}
// Шар Usecase
export interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}
// Шар Interface Adaptor
export class MySQLUserRepository implements UserRepository {
private connection: any; // Насправді потрібно визначити тип об'єкта з'єднання MySQL
constructor(connection: any) {
this.connection = connection;
}
async findById(id: string): Promise<User | null> {
// Реалізація для MySQL
const result = await this.connection.query('SELECT * FROM users WHERE id = ?', [id]);
if (!result.length) return null;
return new User(result[0].id, result[0].name);
}
async save(user: User): Promise<void> {
// Реалізація збереження для MySQL
await this.connection.query('UPDATE users SET name = ? WHERE id = ?', [user.getName(), user.id]);
}
}
// Шар Framework (найзовнішній)
export class UserController {
private readonly userRepository: UserRepository;
constructor(userRepository: UserRepository) {
this.userRepository = userRepository;
}
async getUser(id: string): Promise<User | null> {
try {
return await this.userRepository.findById(id);
} catch (error) {
// Обробка помилок
throw new Error(`Failed to get user: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async updateUserName(id: string, newName: string): Promise<void> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('User not found');
}
user.updateName(newName);
await this.userRepository.save(user);
}
}
// Приклад використання
// Налаштування ін'єкції залежностей
const mysqlConnection = /* Налаштування з'єднання MySQL */;
const userRepository = new MySQLUserRepository(mysqlConnection);
const userController = new UserController(userRepository);
// Використання в API-ендпоінтах чи маршрутах
async function handleGetUser(userId: string) {
try {
const user = await userController.getUser(userId);
if (!user) {
return { status: 404, body: { message: 'User not found' } };
}
return { status: 200, body: { user } };
} catch (error) {
return { status: 500, body: { message: 'Internal server error' } };
}
}
Така структура підвищує тестованість, спрощує підтримку і дозволяє гнучко адаптуватися до змін зовнішніх факторів, як-от зміни у фреймворках чи базах даних. 🙂
Те саме можна сказати і про FE: застосування Clean Architecture допоможе створити стабільні та масштабовані сервіси в довгостроковій перспективі.
Перекладено з: How Clean Architecture Can Transform Your Frontend Development Experience