策略模式(Strategy Pattern)是一种软件设计模式,它定义了算法族,分别封装起来,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。这种模式涉及到三个角色:
- 上下文(Context):持有一个策略类的引用,用来与策略类交互。
- 策略接口(Strategy Interface):定义了每个策略或算法必须遵循的接口。
- 具体策略(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
是策略接口,ShortestPathStrategy
、MinTimeStrategy
和 AvoidHighwaysStrategy
是实现了不同路线规划算法的具体策略类。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:实现具体的日志策略
接下来,我们实现两个具体的日志策略,分别是 ConsoleLogger
和 FileLogger
。
@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;
}
在这个拦截器中,我们通过 rxjs
的 map
操作符来转换处理函数返回的数据。我们把数据包装成一个对象,其中包含数据、时间戳和请求路径。
步骤 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)验证。我们将从它的源码开始分析。
-
ValidationPipe的定义
ValidationPipe
是一个实现了PipeTransform
接口的类。PipeTransform
接口要求实现一个名为transform
的方法。export class ValidationPipe implements PipeTransform<any> {async transform(value, metadata: ArgumentMetadata) {// ...验证逻辑} }
-
使用 Class Validator 进行验证
ValidationPipe
的transform
方法使用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; }
-
调用管道
当请求到达控制器时,NestJS 会根据控制器方法的装饰器(如
@Body
)来确定需要应用哪个管道。在
@Body(new ValidationPipe())
的情况下,NestJS 会创建一个ValidationPipe
实例,并调用它的transform
方法,将传入的请求体作为参数。 -
异常处理
如果验证失败,
ValidationPipe
会抛出一个BadRequestException
。NestJS 捕获这个异常,并根据异常类型生成相应的 HTTP 响应。