赞
踩
Clean Architecture 是 Robert C. Martin 提出的一种软件架构模式,目的是为了将系统分层,实现关注点分离,使系统更易于理解、维护和扩展。该体系结构将系统分为四个层级,从内到外分别是:实体层、用例层、表现层、基础设施(存储库,框架等)。
在本文中,我们将介绍如何使用 Node.js 实现 Clean Architecture,并提供一些示例代码来演示该架构的关键概念。
该项目采用了 Monorepo 结构,使用 Rush.js 进行管理。在 server 文件夹中包含了三个子项目,分别为 core、koa 和 nestjs-app,其中 core 为核心业务逻辑,koa是使用koa+prisma的为底层框架web项目,nestjs-app是使用nestjs + typeorm为底层框架的项目。目的是演示相同的业务逻辑如何桥接不同的框架。
在这个项目中,实体层包含实体对象和相关的业务规则和逻辑,用例层包含系统的用例和业务逻辑,存储库层负责保存和检索数据,表示层则是暴露给外部的http接口。
实现一个帖子发布,浏览功能
- ├── server
- │ ├── core // 核心业务逻辑
- │ │ └── src
- │ │ ├── domain
- │ │ ├── repository
- │ │ └── useCase
- │ ├── koa
- │ │ └── src
- │ │ ├── post
- │ │ └── user
- │ └── nestjs-app
- │ ├── src
- │ ├── post
- │ │ ├── dto
- │ │ └── entities
- │ └── user
- │ └── entities
- └── web
- 复制代码
core:core为核心业务逻辑的代码
koa/nestjs-app: core的实际消费者
在core中,我们有核心的业务逻辑代码。此级别包含域、存储库接口和用例。域包含与实体相关的代码,例如特定的业务模型。存储库包含与外部存储系统的相关接口。用例包含与业务逻辑相关的代码,例如处理业务逻辑、数据验证和调用存储库。
在koa/nestjs-app层面,我们有核心层面的实际消费者。它们根据核心层提供的接口实现特定的路由器和存储库。 使用 Clean Architecture 的主要优点之一是它将业务逻辑与技术实现分开。这意味着您可以轻松地在不同的框架和库之间切换,而无需更改核心业务逻辑。在我们的示例中,我们可以在 koa 和 nestjs-app 之间切换,同时保持相同的核心业务逻辑。
- // server/core/src/domain/post.ts
- import { User } from "./user";
-
- export class Post {
- author: User | null = null;
- content: string = "";
- updateAt: Date = new Date(); // timestamp;
- createdAt: Date = new Date(); // timestamp;
- title: string = "";
- id: number = -1;
- }
-
- // server/core/src/domain/user.ts
- export class User {
- name: string = ''
-
- email: string = ''
-
- id: number = -1
- }
- 复制代码
-
- import { Post } from "../domain/post";
-
- export interface IPostRepository {
- create(post: Post): Promise<boolean>;
-
- find(id: number): Promise<Post>;
-
- update(post: Post): Promise<boolean>;
-
- delete(post: Post): Promise<boolean>;
-
- findMany(options: { authorId: number }): Promise<Post[]>;
- }
-
- ...
- import { User } from "../domain/user";
-
- export interface IUserRepository {
- create(user: User): Promise<boolean>;
- find(id: number): Promise<User>;
- }
- 复制代码
- import { User } from "../domain/user";
- import { IUserRepository } from "../repository/user";
-
- export class UCUser {
- constructor(public userRepo: IUserRepository) {}
-
- find(id: number) {
- return this.userRepo.find(id);
- }
-
- create(name: string, email: string) {
- if (email.includes("@test.com")) {
- const user = new User();
- user.email = email;
- user.name = name;
- return this.userRepo.create(user);
- }
- throw Error("Please use legal email");
- }
- }
- 复制代码
- // server/koa/src/user/user.repo.ts
- import { PrismaClient } from "@prisma/client";
- import { IUserRepository, User } from "core";
-
- export class UserRepository implements IUserRepository {
- prisma = new PrismaClient();
-
- async create(user: User): Promise<boolean> {
- const d = await this.prisma.user_orm_entity.create({
- data: {
- email: user.email,
- name: user.name,
- },
- });
-
- return !!d;
- }
-
- async find(id: number): Promise<User> {
- const d = await this.prisma.user_orm_entity.findFirst({
- where: {
- id: id,
- },
- });
-
- if (d) {
- const u = new User();
- u.email = d?.email;
- u.id = d?.id;
- u.name = d?.name;
- return u;
- }
- throw Error("user id " + id + "not found");
- }
- }
- 复制代码
- // server/koa/src/user/user.controller.ts
- import Router from "@koa/router";
- import { UCUser } from "core";
- import { UserRepository } from "./user.repo";
-
- export const userRouter = new Router({
- prefix: "/user",
- });
-
- userRouter.get("/:id", async (ctx, next) => {
- try {
- const service = new UCUser(new UserRepository());
- if (ctx.params.id) {
- const u = await service.find(+ctx.params.id);
- ctx.response.body = JSON.stringify(u);
- }
- } catch (e) {
- ctx.throw(400, "some error on get user", e.message);
- }
- await next();
- });
- 复制代码
请注意,在实际项目中,我们不会将核心业务逻辑放在单独的仓库中(即core),这只是为了演示在不同框架下使用相同的业务逻辑
通过将业务逻辑与框架分离,您可以轻松地在不同的框架和库之间切换,而无需更改核心业务逻辑。如果您希望构建可扩展且可维护的应用程序,那么Clean Architecture 绝对值得考虑。
如果想要演示如何接入其他框架,可以在评论区提出
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。