APIs是现代应用的支柱,但有时不仅需要进行 CRUD 操作,还需要完成更多任务。
假设一个场景,你需要更新用户的资料,并且发送后台通知和电子邮件——高效且不干扰主要请求。
我们来看看如何通过一种这样的结构化且受函数式编程启发的方式做到这一点。
问题说明我们需要实现一个用户更新API,它需要具备以下功能:
- 接受更新用户名或电话号码的请求。
- 识别更改的字段。
- 根据识别出的更改更新数据库。
- 获取用户的设备令牌,并在后台发送通知。
- 异步发送邮件给用户。
- 返回适当的响应给客户端。
约翰·卡马克,一位传奇游戏开发者,他认为函数式编程可以减少那些讨厌的副作用,让软件更可靠。
在他看来,软件开发中许多问题是因为程序员没有充分理解他们的代码可能进入的所有状态。这个问题在多线程环境中更加严重,竞态条件可能导致不可预测的结果。
函数式编程通过让状态变得清晰和减少不必要的副作用来解决这些问题。
即使我们不使用Haskell或Lisp,我们仍然可以在主流语言如JavaScript和TypeScript中应用函数式编程的思想。
正如卡马克所说的一样,"无论你使用什么编程语言,用函数式编程风格都会给你带来好处。"
通过采用纯函数、不可变数据和职责分离的设计,我们的 API 更易于测试、维护和扩展,从而提高了 API 的可测试性、可维护性和可扩展性。
1. 路线定义
在这些后端框架中,如 Nest.js、Express 或 Django,我们定义一个“接口”:
PATCH 请求 /api/user/update
切换到全屏,退出全屏
2. 请求体
请求中应包含需要更新的字段信息:
{
// "userId": "12345",
"username": "newUser123",
"phoneNumber": "9876543210"
}
进入全屏。退出全屏。
3. Nest.js 示例实现
我们采用结构化的方法来分离关注点,确保我们的功能模块既易于测试又可重用。
import { Controller, Patch, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { NotificationService } from './notification.service';
import { EmailService } from './email.service';
@Controller('user')
export class UserController {
constructor(
private readonly userService: UserService,
private readonly notificationService: NotificationService,
private readonly emailService: EmailService,
) {}
@Patch('update')
async updateUser(@Body() body: any) {
const { userId, username, phoneNumber } = body;
// 检查哪些字段发生了变化
const updates: Partial<User> = {};
if (username) updates['username'] = username;
if (phoneNumber) updates['phoneNumber'] = phoneNumber;
// 以纯函数方式更新用户信息到数据库
const updatedUser = await this.userService.updateUser(userId, updates);
// 获取用户token并在后台发送更新通知
this.notificationService.sendUserUpdateNotification(userId);
// 在后台发送更新邮件给用户
this.emailService.sendUpdateEmail(updatedUser);
return { message: '用户信息已成功更新', data: updatedUser };
}
}
进入全屏 退出全屏
4. 数据库更新功能模块(纯函数方式)
以下的函数更新数据库中的数据并保证数据的一致性。
// 更新用户信息的异步方法
async updateUser(userId: string, updates: Partial<User>) {
// 更新用户的ID和更新的内容
return this.userModel.findByIdAndUpdate(userId, updates, { new: true });
}
全屏切换进入 全屏切换退出
约翰·卡马克强调,纯函数仅对其输入进行操作并返回计算结果,不会修改任何共享状态。这种方法确保了:
- 线程安全: 没有意外的副作用。
- 容易在新环境中使用:
- 对于相同的输入,总是给出相同的输出。
5. 背景通知管理
我们先拿到用户的令牌,然后发送推送消息,不会影响到主要请求。
async sendUserUpdateNotification(userId: string) {
const userDevice = await this.userDeviceModel.findOne({ userId });
if (userDevice?.token) {
pushNotificationService.send(userDevice.token, '您的资料已被更新。如果不是您自己更新的,请联系客服。');
}
}
点击进入全屏,点击退出全屏
6. 发邮件(后台运行)
电子邮件传输可能很慢,所以我们将其放入任务队列中,比如 BullMQ / Kafka。
async sendUpdateEmail(user: User) {
emailQueue.add('sendEmail', {
to: user.email,
subject: '资料更新',
body: '您的资料已成功更新。如果不是您本人更新,请联系客服。',
});
}
切换到/退出全屏
务实的平衡并不是所有的东西都可以纯粹是为了功能性——实际的应用程序需要与数据库、文件系统和外部服务交互。
正如卡马克所言,“从更广泛的角度看,避免最糟糕的情况发生通常比在有限情况下追求完美更重要。”
与其在每个地方都强制保持严格纯洁性,我们的目标是在我们应用的关键部分减少副作用,同时以可控的方式处理必要的更改。
- 非阻塞特性: 后台任务确保 API 始终保持响应。
- 职责分离: 更新逻辑、通知和邮件处理各自独立。
- 函数式思维: 数据库更新函数是一个纯函数,这使得测试更加容易。
- 可扩展性: 后台处理比同步执行具有更好的扩展能力。
- 代码可靠性: 减少副作用使调试更加简单。
如果你对后端开发中的函数式编程感兴趣,并想更深入地探索,可以考虑可以参考一些在线教程或书籍。
- 实现后台任务中的重试机制和错误处理。
- 使用 Kafka 或 RabbitMQ 的事件驱动架构。
- 研究 JavaScript 中的函数式编程库,如 Ramda 和 Lodash/fp 这样的库。
约翰·卡马克曾说过,用函数式编程风格编写代码确实有好处。
你最好在方便的时候就去做,并且在不方便的时候要仔细考虑一下这个决定。
我正在开发一个叫做 LiveAPI 的超级方便工具。
LiveAPI 帮助你在几分钟内整理好所有后端 API 的文档
使用LiveAPI(实时API),您可以轻松生成互动的API文档,让使用者在浏览器中直接运行API。
如果你烦透了手动创建API的文档,这个工具可能让你更轻松地搞定API文档。
共同学习,写下你的评论
评论加载中...
作者其他优质文章