前端 nestjs SOLID原则

109 阅读6分钟

前端的 SOLID 原则是面向对象编程中的五个设计原则,旨在提升代码的可维护性和扩展性。尽管这些原则最初针对后端开发,但它们同样适用于前端开发。以下是 SOLID 原则及其在前端中的应用:

  1. 单一职责原则(Single Responsibility Principle, SRP)
  2. 开闭原则(Open/Closed Principle, OCP)
  3. 里氏替换原则(Liskov Substitution Principle, LSP)
  4. 接口隔离原则(Interface Segregation Principle, ISP)
  5. 依赖倒置原则(Dependency Inversion Principle, DIP)

单一职责原则 (SRP - Single Responsibility Principle)

  • 定义:一个类或模块应只有一个职责。
  • 前端应用:每个组件、函数或模块应专注于单一功能。
// Not following SRP
class User {
  constructor(public name: string, public email: string) {}

  save() {
    // Save user to database
  }

  sendEmail() {
    // Send welcome email
  }
}
// Following SRP
class User {
  constructor(public name: string, public email: string) {}
}

class UserRepository {
  save(user: User) {
    // Save user to database
  }
}

class EmailService {
  sendWelcomeEmail(user: User) {
    // Send welcome email
  }
}

在代码中,User 类只负责存储用户的基本信息(如 nameemail),UserRepository 类只负责将用户数据保存到数据库,而 EmailService 类只负责发送欢迎邮件。 通过这种方式,代码更易于维护和扩展。如果需要修改数据库保存逻辑,只需修改 UserRepository 类;如果需要调整邮件发送逻辑,只需修改 EmailService 类,而不会影响其他部分的代码。

在Nest.js中,这意味着控制器、服务和模块等应当各自负责不同的职责。例如,控制器负责处理HTTP请求,服务负责业务逻辑,模块负责组织代码。

// user.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';
import { ApiTags } from '@nestjs/swagger';

@ApiTags('users')
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  @ApiOperation({ summary: '获取所有用户' })
  @ApiResponse({ status: 200, description: '返回所有用户列表', type: [User] })
  findAll() {
    return this.userService.findAll();
  }
}

// user.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  findAll() {
    return `This action returns all user`;
  }
}

开放/封闭原则 (OCP - Open/Closed Principle)

  • 定义:软件实体应对扩展开放,对修改封闭。应通过扩展已有代码的方式来实现新的功能,而不是修改已有的代码。
  • 前端应用:组件应设计为允许通过扩展(如继承或组合)添加新功能,而不需修改现有代码。
// Not following OCP
class Rectangle {
  constructor(public width: number, public height: number) {}
}

class AreaCalculator {
  calculateArea(shape: any) {
    if (shape instanceof Rectangle) {
      return shape.width * shape.height;
    }
    // Add other shapes here...
  }
}
// Following OCP
interface Shape {
  calculateArea(): number;
}

class Rectangle implements Shape {
  constructor(public width: number, public height: number) {}
  calculateArea(): number {
    return this.width * this.height;
  }
}

class Circle implements Shape {
  constructor(public radius: number) {}
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

class AreaCalculator {
  calculateArea(shape: Shape): number {
    return shape.calculateArea();
  }
}

在代码中,Shape 接口定义了一个通用的 calculateArea 方法,RectangleCircle 类分别实现了这个接口,提供了各自计算面积的具体逻辑。AreaCalculator 类通过依赖 Shape 接口来计算面积,而不是直接依赖具体的形状类。

这种设计使得系统对扩展开放:如果需要添加新的形状(例如三角形),只需创建一个新的类实现 Shape 接口,而无需修改现有的 AreaCalculator 类。同时,系统对修改封闭:现有的 RectangleCircle 类以及 AreaCalculator 类不需要因为新形状的加入而修改。

在Nest.js中,可以通过继承、装饰器和依赖注入来实现对已有功能的扩展。

// base.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class BaseService {
  findAll() {
    return ['base item'];
  }
}

// extended.service.ts
import { Injectable } from '@nestjs/common';
import { BaseService } from './base.service';

@Injectable()
export class ExtendedService extends BaseService {
  findAll() {
    const items = super.findAll();
    return [...items, 'extended item'];
  }
}

里氏替换原则(Liskov Substitution Principle, LSP)

  • 定义:子类应能替换父类而不影响程序正确性。
  • 前端应用:在使用继承或组合时,确保子组件或扩展组件能无缝替换父组件。
// Not following LSP
class Bird {
  fly() {
    // Bird can fly
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error('Penguins cannot fly');
  }
}
// Following LSP
class Bird {
  move() {
    // Bird can move
  }
}

class FlyingBird extends Bird {
  fly() {
    // Bird can fly
  }
}

class Penguin extends Bird {
  move() {
    // Penguins can move
  }
}

在代码中,Bird 是一个基类,定义了 move 方法。FlyingBird 继承自 Bird,并扩展了 fly 方法,表示会飞的鸟。Penguin 也继承自 Bird,但重写了 move 方法,因为企鹅不会飞,但可以移动。这种设计符合里氏替换原则:FlyingBirdPenguin 都可以替换 Bird 类,而不会破坏程序的逻辑。例如,如果一个函数接受 Bird 类型的参数,无论是传入 FlyingBird 还是 Penguin,程序都能正常运行。

在Nest.js中,通过接口和抽象类可以实现里氏替换原则。例如,可以定义一个接口或抽象类,然后在不同的实现类中遵循这个接口或抽象类的契约。

// logger.interface.ts
export interface Logger {
  log(message: string): void;
}

// console.logger.ts
import { Logger } from './logger.interface';

export class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ConsoleLogger } from './console.logger';

@Module({
  controllers: [AppController],
  providers: [ConsoleLogger],
})
export class AppModule {}

接口隔离原则(Interface Segregation Principle, ISP)

  • 定义:客户端不应依赖不需要的接口。
  • 前端应用:组件应提供最小且必要的接口,避免臃肿。
// Not following ISP
interface Animal {
  eat(): void;
  fly(): void;
}

class Dog implements Animal {
  eat() {
    // Dog can eat
  }
  fly() {
    // Dog cannot fly, but forced to implement
  }
}
// Following ISP
interface Eater {
  eat(): void;
}

interface Flyer {
  fly(): void;
}

class Dog implements Eater {
  eat() {
    // Dog can eat
  }
}

class Bird implements Eater, Flyer {
  eat() {
    // Bird can eat
  }
  fly() {
    // Bird can fly
  }
}

在代码中,Eater 接口定义了 eat 方法,表示“吃”的行为,而 Flyer 接口定义了 fly 方法,表示“飞”的行为。Dog 类实现了 Eater 接口,因为狗可以吃东西,但狗不会飞,所以不需要实现 Flyer 接口。Bird 类同时实现了 EaterFlyer 接口,因为鸟既可以吃东西,也可以飞。

这种设计符合接口隔离原则:每个类只依赖于它们实际需要的接口。例如,Dog 类不需要实现 Flyer 接口,因为它不会飞;而 Bird 类可以根据需要选择实现多个接口。

在Nest.js中,通过定义小而专用的接口,可以实现接口隔离原则。

// auth.interface.ts
export interface AuthService {
  login(username: string, password: string): boolean;
  register(username: string, password: string): boolean;
}

// auth.service.ts
import { Injectable } from '@nestjs/common';
import { AuthService } from './auth.interface';

@Injectable()
export class AuthServiceImpl implements AuthService {
  login(username: string, password: string): boolean {
    // 登录逻辑
    return true;
  }
  register(username: string, password: string): boolean {
    // 注册逻辑
    return true;
  }
}

依赖倒置原则(Dependency Inversion Principle, DIP)

  • 定义:高层模块不应依赖低层模块,两者都应依赖抽象。换句话说,依赖关系应该是通过抽象来实现的,而不是通过具体实现。
  • 前端应用:组件应依赖抽象接口而非具体实现。
class MySQLDatabase {
  connect() {
    // Connect to MySQL database
  }
}

class UserRepository {
  private database: MySQLDatabase;

  constructor() {
    this.database = new MySQLDatabase();
  }

  save(user: any) {
    this.database.connect();
    // Save user to database
  }
}
// Following DIP
interface Database {
  connect(): void;
}

class MySQLDatabase implements Database {
  connect() {
    // Connect to MySQL database
  }
}

class UserRepository {
  private database: Database;

  constructor(database: Database) {
    this.database = database;
  }

  save(user: any) {
    this.database.connect();
    // Save user to database
  }
}

在代码中,Database 是一个接口,定义了 connect 方法,表示数据库连接的行为。MySQLDatabase 类实现了 Database 接口,提供了具体的 MySQL 数据库连接逻辑。UserRepository 类是一个高层模块,它依赖于 Database 接口,而不是具体的 MySQLDatabase 类。

这种设计符合依赖倒置原则:UserRepository 不直接依赖具体的数据库实现(如 MySQLDatabase),而是依赖抽象的 Database 接口。如果需要更换数据库(例如从 MySQL 切换到 PostgreSQL),只需创建一个新的类实现 Database 接口,并注入到 UserRepository 中,而无需修改 UserRepository 的代码。

在Nest.js中,通过依赖注入机制可以实现依赖倒置原则。例如,通过在构造函数中注入接口或抽象类,而不是具体实现类。

// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AuthService } from './auth.interface';

@Controller('auth')
export class AppController {
  constructor(private readonly authService: AuthService) {}

  @Get('login')
  login() {
    return this.authService.login('user', 'pass');
  }
}

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AuthServiceImpl } from './auth.service';

@Module({
  controllers: [AppController],
  providers: [AuthServiceImpl],
})
export class AppModule {}

总结

SOLID 原则在前端开发中的应用有助于构建更清晰、易维护和可扩展的代码。通过遵循这些原则,开发者可以提升代码质量,降低维护成本。