angular
March 4

Принципы SOLID, LIFT, and FIRST на примере Angular

Принципы SOLID, LIFT и FIRST - это наборы лучших практик и принципов проектирования в области разработки программного обеспечения, направленные на улучшение кодовой базы, читаемости и масштабируемости вашего кода. Каждый из них имеет свою цель, но дополняют друг друга, стремясь к созданию качественного, управляемого и надежного программного обеспечения.

SOLID:
Это акроним для пяти принципов проектирования в объектно-ориентированном программировании и дизайне. Эти принципы помогают разработчикам создавать системы, которые легко поддерживать, понимать и расширять со временем.

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

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

Хотя SOLID, LIFT и FIRST могут фокусироваться на различных аспектах процесса разработки программного обеспечения, они разделяют одну и ту же цель: улучшение качества кода. Теперь, когда они используются вместе, эти принципы могут помочь создать код, который является чистым, эффективным и поддерживаемым.

Комбинация хорошего дизайна (SOLID), четкой организации (LIFT) и эффективного тестирования (FIRST) может значительно повысить надежность и качество программного проекта.

S.O.L.I.D

SOLID описывает пять основных принципов объектно-ориентированного проектирования ПО. Он пропагандирует метод разработки, который позволяет создавать программное обеспечение, которое легко расширять и более понятно для чтения.

S - (SRP) Принцип единственной обязанности (Single Responsibility Principle)
Класс должен выполнять только ОДНУ задачу или "обязанность"

Плохой пример:

// Violates SRP
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  constructor(private http: HttpClient) { }

  getUserData(userId: string) {
    return this.http.get(`https://example.com/user/${userId}`);
  }

  saveUserDataInLocalStorage(userData) {
    localStorage.setItem('user', JSON.stringify(userData));
  }
}

Хороший пример:

// Follows SRP
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root',
})
export class UserDataService {
  constructor(private http: HttpClient) { }

  getUserData(userId: string) {
    return this.http.get(`https://example.com/user/${userId}`);
  }
}

@Injectable({
  providedIn: 'root',
})
export class LocalStorageService {
  saveUserDataInLocalStorage(userData) {
    localStorage.setItem('user', JSON.stringify(userData));
  }
}


O - (OCP) Принцип открытости/закрытости (Open/Closed Principle)
Сущности программного обеспечения (классы, модули, функции и т. д.) должны быть открыты для расширения (наследование, расширение, интерфейсы и т. д.), но закрыты для модификации (работает - не трогай).

В Angular, один из способов применения OCP - использование внедрения зависимостей (DI). Внедрение зависимостей позволяет нам предоставлять различные реализации сервиса без изменения кода самого сервиса.

В FallbackUserService мы используем исходный UserService для получения данных о пользователе. Если он не удается, мы предоставляем данные о пользователе по умолчанию. Таким образом, UserService остается "закрытым" для модификации, но "открытым" для расширения через FallbackUserService.

@Injectable({
  providedIn: 'root',
})
export class FallbackUserService {
  // UserService remains "closed" for changes but "open" for extension
  constructor(private userService: UserService) { }

  getUserData(userId: string) {
    return this.userService.getUserData(userId).pipe(
      catchError(() => of({ id: userId, name: 'Default User' }))
    );
  }
}

L - (LSP) Принцип подстановки Лисков (Liskov Substitution Principle, LSP) Объекты базового класса должны быть заменяемы объектами любого из своих подклассов без нарушения функциональности программы, так как подкласс должен вести себя так же, как его Родитель". Кратко говоря, подкласс должен быть способен делать то же самое, что и его родитель или суперкласс, но не обязательно наоборот.

Например: VIP-пользователи, у которых есть свой собственный API-эндпоинт и, возможно, дополнительные поля данных.

Если мы не применим принцип LSP, мы могли бы просто добавить еще один метод в сервис UserDataService, например, getVIPUserData(). Однако с учетом LSP мы вместо этого создадим новый класс, расширяющий функционал UserDataService:

@Injectable({
  providedIn: 'root',
})
export class UserDataService {
  constructor(protected http: HttpClient) { }

  getUserData(userId: string) {
    return this.http.get(`https://example.com/user/${userId}`);
  }
}

@Injectable({
  providedIn: 'root',
})
export class VIPUserDataService extends UserDataService {
  getUserData(userId: string) {
    return this.http.get(`https://example.com/vipuser/${userId}`);
  }
}


I - Принцип разделения интерфейса (ISP)
Клиенты (или, в данном случае, компоненты или другие сервисы) не должны зависеть от интерфейсов, которые им не нужны. В основном, лучше иметь много маленьких, "специфичных" интерфейсов, чем один большой, общий, "универсальный" интерфейс.

// Interface for reading user data
export interface UserDataReader {
  getUserData(userId: string): Observable<User>;
}

// Interface for managing user data
export interface UserDataManager {
  createUser(user: User): Observable<User>;
  updateUser(user: User): Observable<User>;
  deleteUser(userId: string): Observable<void>;
}
@Injectable({
  providedIn: 'root',
})
export class UserDataService implements UserDataReader, UserDataManager {
  constructor(private http: HttpClient) { }

  getUserData(userId: string): Observable<User> {
    return this.http.get<User>(`https://example.com/user/${userId}`);
  }

  createUser(user: User): Observable<User> {
    return this.http.post<User>('https://example.com/user', user);
  }

  updateUser(user: User): Observable<User> {
    return this.http.put<User>(`https://example.com/user/${user.id}`, user);
  }

  deleteUser(userId: string): Observable<void> {
    return this.http.delete<void>(`https://example.com/user/${userId}`);
  }
}

В примере любой компонент или сервис, который только нуждается в чтении данных пользователя, может зависеть от UserDataReader и не будет вынужден также зависеть от методов для создания, обновления и удаления данных пользователя (которые являются частью UserDataManager).

Это соответствует ISP и делает наш код более гибким, легким для понимания и менее подверженным поломкам из-за изменений.

D - (DIP) Принцип инверсии зависимостей

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

Пример: Внедрение зависимостей - один из методов, следующих этот принцип.

Чтобы реализовать принцип инверсии зависимостей в Angular на примере нашего UserDataService, сначала нам нужно создать абстрактный сервис (интерфейс в TypeScript). Этот интерфейс будет контрактом, который любой "сервис данных о пользователе" должен реализовать:

export interface IUserDataService {
  getUserData(userId: string);
}

Затем мы заставляем наш UserDataService реализовывать этот интерфейс:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root',
})
export class UserDataService implements IUserDataService {
  constructor(private http: HttpClient) { }

  getUserData(userId: string) {
    return this.http.get(`https://example.com/user/${userId}`);
  }
}

Теперь любой компонент или сервис, который использует UserDataService, должен зависеть от IUserDataService (абстракция), а не непосредственно от UserDataService (деталь).

import { Component, OnInit } from '@angular/core';
import { IUserDataService } from './i-user-data.service';

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
  constructor(private userDataService: IUserDataService) { } // depends on the abstraction, not the detail

  ngOnInit() {
    this.userDataService.getUserData('123').subscribe(
      data => {...}
    );
  }
}

Таким образом, компонент UserComponent зависит от абстракции (IUserDataService), а не от деталей (UserDataService). Если нам когда-либо понадобится изменить способ получения данных пользователя (например, если мы захотим получать данные из локального хранилища вместо HTTP-сервера), мы можем создать новый сервис, который реализует интерфейс IUserDataService, и компонент UserComponent не потребуется изменяться.

FIRST

Это принципы, которые помогают создавать чистый, поддерживаемый код. Он соответствует многим концепциям в SOLID, а так же другим лучшим практикам в разработке программного обеспечения. Давайте разберем каждый принцип подробнее. FIRST - это акроним, который означает следующее: Сосредоточенность, Независимость, Повторное использование, Размер и Тестируемость компонентов.

F - каждый объект должен иметь единственную обязанность (Этот принцип тесно связан с Принципом единственной ответственности (SRP) из SOLID)

I - сделайте его более независимым (Independent) и тестируемым (Цель здесь - минимизировать зависимости ваших компонентов)

R - Повторное использование кода может сэкономить вам время и сделать его переносимым

S - Меньшие API легче изучать и обучать других. Это обычно ппомогает, если вы делаете одно дело и делаете это хорошо. (Этот принцип также соответствует SRP, поскольку компонент, который делает только одно дело, склонен к тому, чтобы быть маленьким.)

T - Тестируйте свой код (Если компонент независим, повторно используем и мал, тестирование кода также будет проще, поскольку он будет предсказуемым, подобно принципу LSP)

LIFT

L - Легко находить наш код

I - Идентифицировать код с первого взгляда

F - Плоская структура насколько это возможно

T - Стремиться к принципу DRY (Don't Repeat Yourself)

Это вольный перевод оригинального материала