nestjs之策略模式的应用

策略模式(Strategy Pattern)是一种软件设计模式,它定义了算法族,分别封装起来,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。这种模式涉及到三个角色:

  1. 上下文(Context):持有一个策略类的引用,用来与策略类交互。
  2. 策略接口(Strategy Interface):定义了每个策略或算法必须遵循的接口。
  3. 具体策略(Concrete Strategies):实现策略接口的类,提供具体的算法实现。

策略模式的优点

  • 分离算法:策略模式通过分离算法和上下文来提高内聚性和灵活性。
  • 易于扩展:可以定义新的策略而不影响到其他的代码。
  • 避免条件语句:策略模式避免了使用多重条件选择语句。

举例说明

假设我们正在开发一个导航系统,需要支持多种路线规划算法,例如最短路径、最少时间和避开高速。
当使用 TypeScript 来实现策略模式时,基本的模式和概念与 Java 类似,只是语法稍有不同。以下是使用 TypeScript 实现策略模式的示例:

首先,定义策略接口和具体策略类:

// 策略接口
interface RouteStrategy {
  buildRoute(pointA: string, pointB: string): string;
}

// 具体策略类 - 最短路径策略
class ShortestPathStrategy implements RouteStrategy {
  buildRoute(pointA: string, pointB: string): string {
    return `最短路径从 ${pointA}${pointB}`;
  }
}

// 具体策略类 - 最少时间策略
class MinTimeStrategy implements RouteStrategy {
  buildRoute(pointA: string, pointB: string): string {
    return `最少时间路径从 ${pointA}${pointB}`;
  }
}

// 具体策略类 - 避开高速策略
class AvoidHighwaysStrategy implements RouteStrategy {
  buildRoute(pointA: string, pointB: string): string {
    return `避开高速的路径从 ${pointA}${pointB}`;
  }
}

接下来,创建上下文类和使用策略模式:

// 上下文类
class NavigationContext {
  private strategy: RouteStrategy;

  setRouteStrategy(strategy: RouteStrategy): void {
    this.strategy = strategy;
  }

  buildRoute(pointA: string, pointB: string): string {
    return this.strategy.buildRoute(pointA, pointB);
  }
}

// 使用策略模式
const context = new NavigationContext();

// 选择最短路径策略
context.setRouteStrategy(new ShortestPathStrategy());
console.log(context.buildRoute("起点", "终点"));

// 切换到最少时间策略
context.setRouteStrategy(new MinTimeStrategy());
console.log(context.buildRoute("起点", "终点"));

// 切换到避开高速策略
context.setRouteStrategy(new AvoidHighwaysStrategy());
console.log(context.buildRoute("起点", "终点"));

在这个例子中,RouteStrategy 是策略接口,ShortestPathStrategyMinTimeStrategyAvoidHighwaysStrategy 是实现了不同路线规划算法的具体策略类。NavigationContext 是上下文,负责接受不同的策略并使用它们来构建路线。这样,当需要改变路线规划算法时,只需更换不同的策略类即可,无需修改 NavigationContext 的代码。

理解和运用这些设计模式可以帮助你更有效地使用 NestJS 构建可维护和可扩展的应用程序。

在 NestJS 中,策略模式主要用于以下几个领域:

Authentication

在 NestJS 中实现策略模式主要是通过 Guards 来完成的,特别是在处理授权(Authorization)时。我们可以通过定义不同的 Guards 来实现不同的授权策略,例如基于角色的授权(RBAC)或者是基于权限的授权。

示例:基于角色的访问控制(RBAC)

假设我们有一个简单的应用程序,它有两种用户角色:普通用户(User)和管理员(Admin)。我们想要实现的是,某些操作只能由管理员执行。

步骤 1:定义角色

首先,我们定义一个角色枚举(Enum)。

export enum Role {
  User = 'user',
  Admin = 'admin',
}
步骤 2:创建 Roles 装饰器

接下来,我们创建一个自定义装饰器来标记特定的路由需要特定的角色。

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
步骤 3:实现 RolesGuard

然后,我们实现一个 RolesGuard,它将检查用户是否具有访问特定路由所需的角色。

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<Role[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return roles.some(role => user.roles?.includes(role));
  }
}

在这个 Guard 中,我们使用 Reflector 来获取与当前路由处理程序相关联的角色。然后,我们检查当前用户是否具有这些角色之一。

步骤 4:应用 RolesGuard

最后,我们需要在模块中注册这个 Guard 并应用到具体的路由上。

// 在模块中
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

// 在控制器中
@Controller('items')
export class ItemsController {
  @Get()
  @Roles(Role.Admin) // 只有管理员可以访问
  findAll() {
    // ...
  }
}

在这个例子中,RolesGuard 作为一个策略,用于控制对特定路由的访问。通过简单地更改 @Roles 装饰器中的参数,我们可以轻松地改变访问控制的策略,而无需修改其他业务逻辑。

Logging

在 NestJS 中实现日志策略模式通常涉及到自定义日志服务。这样,你可以根据需要切换或扩展不同的日志策略,例如输出日志到控制台、文件、远程服务器等。

示例:自定义日志服务

假设我们需要实现两种日志策略:一种是简单地将日志输出到控制台,另一种是将日志记录到文件中。

步骤 1:定义日志接口

首先,我们定义一个日志接口(LoggerService),它描述了日志服务应该实现的方法。

interface LoggerService {
  log(message: string): void;
  error(message: string, trace: string): void;
  warn(message: string): void;
  debug(message: string): void;
  verbose(message: string): void;
}
步骤 2:实现具体的日志策略

接下来,我们实现两个具体的日志策略,分别是 ConsoleLoggerFileLogger

@Injectable()
class ConsoleLogger implements LoggerService {
  log(message: string) { console.log(message); }
  // ...实现其他方法
}

@Injectable()
class FileLogger implements LoggerService {
  log(message: string) {
    // 将消息写入文件
  }
  // ...实现其他方法
}
步骤 3:动态选择日志策略

然后,我们可以根据需要在应用中动态选择使用哪个日志策略。

@Module({
  providers: [
    {
      provide: 'LoggerService',
      useClass: process.env.NODE_ENV === 'development' ? ConsoleLogger : FileLogger,
    },
  ],
})
export class AppModule {}

在这个模块中,我们根据环境变量来决定使用 ConsoleLogger 还是 FileLogger

步骤 4:使用日志服务

在应用的其他部分,我们可以注入并使用 LoggerService

@Controller('items')
export class ItemsController {
  constructor(@Inject('LoggerService') private logger: LoggerService) {}

  @Get()
  findAll() {
    this.logger.log('Fetching all items');
    // ...业务逻辑
  }
}

在这个控制器中,我们通过构造函数注入了 LoggerService。不论底层使用的是哪种日志策略,我们都可以通过相同的方式记录日志。

Exception Handling

在 NestJS 中,异常处理通常通过异常过滤器(Exception Filters)来实现,这可以被视为一种策略模式的应用。异常过滤器允许你定义不同的处理策略来处理不同类型的异常。以下是一个详细的例子,展示如何在 NestJS 中使用异常过滤器来实现异常处理的策略模式。

示例:自定义异常过滤器

假设我们的应用需要特定的处理方式来处理数据库异常和 HTTP 异常。

步骤 1:创建自定义异常过滤器

首先,我们创建两个异常过滤器,一个用于处理数据库异常,另一个用于处理 HTTP 异常。

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        message: exception.message,
      });
  }
}

@Catch(DatabaseException)
export class DatabaseExceptionFilter implements ExceptionFilter {
  catch(exception: DatabaseException, host: ArgumentsHost) {
    // 处理数据库异常的逻辑
  }
}
步骤 2:注册异常过滤器

接下来,我们需要在应用中注册这些异常过滤器。你可以全局注册或针对特定控制器或路由注册。

  • 全局注册:

    @Module({
      // ...
      providers: [
        {
          provide: APP_FILTER,
          useClass: HttpExceptionFilter,
        },
        {
          provide: APP_FILTER,
          useClass: DatabaseExceptionFilter,
        },
      ],
    })
    export class AppModule {}
    
  • 针对特定控制器的注册:

    @Controller('users')
    @UseFilters(new HttpExceptionFilter(), new DatabaseExceptionFilter())
    export class UsersController {
      // ...
    }
    
步骤 3:触发异常

在应用的任何地方抛出异常,对应的过滤器将会捕获并处理它。

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id') id: string): string {
    throw new HttpException('User not found', HttpStatus.NOT_FOUND);
  }
}

在这个例子中,当 findOne 方法抛出 HttpException 时,HttpExceptionFilter 会被触发,并按照其逻辑处理异常。

Request Processing

在 NestJS 中,请求处理(Request Processing)通常涉及拦截器(Interceptors),这些拦截器可以被视为一种策略模式的实现。拦截器允许你在请求处理流程中插入自定义逻辑,比如日志记录、响应转换、错误处理等。接下来我将通过一个详细的例子来说明如何在 NestJS 中使用拦截器实现请求处理的策略。

示例:响应转换拦截器

假设我们需要一个拦截器来统一格式化所有 API 响应。这个拦截器将拦截出站响应,并将其转换为一个标准的格式。

步骤 1:创建拦截器

首先,我们创建一个名为 TransformInterceptor 的拦截器。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => ({
        data,
        timestamp: new Date().toISOString(),
        path: context.switchToHttp().getRequest().url,
      }))
    );
  }
}

interface Response<T> {
  data: T;
  timestamp: string;
  path: string;
}

在这个拦截器中,我们通过 rxjsmap 操作符来转换处理函数返回的数据。我们把数据包装成一个对象,其中包含数据、时间戳和请求路径。

步骤 2:注册拦截器

接下来,我们需要在应用中注册这个拦截器。有两种方式可以注册拦截器:全局注册和针对特定路由的注册。

  • 全局注册:

    @Module({
      // ...
      providers: [
        {
          provide: APP_INTERCEPTOR,
          useClass: TransformInterceptor,
        },
      ],
    })
    export class AppModule {}
    
  • 针对特定路由的注册:

    在特定控制器或者处理函数上使用拦截器:

    @Controller('items')
    @UseInterceptors(TransformInterceptor)
    export class ItemsController {
      // ...
    }
    
步骤 3:使用拦截器

一旦拦截器被注册,它就会自动应用于你的请求处理流程。在上面的例子中,任何通过 ItemsController 的响应都会被 TransformInterceptor 拦截并格式化。

Validation and Transformation

从源码层面详细分析 NestJS 中的管道(Pipes)是一个涉及到多个文件和类的复杂过程,但我会尽量简化并解释关键部分。

管道的基本原理

在 NestJS 中,管道(Pipes)是负责处理输入数据的中间件,它们在控制器处理函数执行之前运行。管道可以执行数据转换或数据验证。当你在控制器的参数前使用管道,NestJS 会在将请求传递给处理函数之前执行这些管道。

核心类:ValidationPipe

ValidationPipe 为例,这是一个内置的管道,通常用于 DTO(Data Transfer Object)验证。我们将从它的源码开始分析。

  1. ValidationPipe的定义

    ValidationPipe 是一个实现了 PipeTransform 接口的类。PipeTransform 接口要求实现一个名为 transform 的方法。

    export class ValidationPipe implements PipeTransform<any> {
      async transform(value, metadata: ArgumentMetadata) {
        // ...验证逻辑
      }
    }
    
  2. 使用 Class Validator 进行验证

    ValidationPipetransform 方法使用 class-validator 库来验证输入数据。如果数据不符合 DTO 定义的规则,它会抛出异常。

    import { validate } from 'class-validator';
    
    async transform(value, { metatype }): Promise<any> {
      if (!metatype || !this.toValidate(metatype)) {
        return value;
      }
      const object = plainToClass(metatype, value);
      const errors = await validate(object);
      if (errors.length > 0) {
        throw new BadRequestException('Validation failed');
      }
      return value;
    }
    
  3. 调用管道

    当请求到达控制器时,NestJS 会根据控制器方法的装饰器(如 @Body)来确定需要应用哪个管道。

    @Body(new ValidationPipe()) 的情况下,NestJS 会创建一个 ValidationPipe 实例,并调用它的 transform 方法,将传入的请求体作为参数。

  4. 异常处理

    如果验证失败,ValidationPipe 会抛出一个 BadRequestException。NestJS 捕获这个异常,并根据异常类型生成相应的 HTTP 响应。

你可能感兴趣的:(nestjs原理,策略模式,javascript,前端)