1.6 引入DAO层

87 阅读4分钟

上节最后提到:

再思考下,上面的代码还有什么问题?还有什么能优化的吗?

问题

先说2个问题。

1. id是自增数字

  1. 每次生成,都有两次IO(先读一次,再写一次)。当然,性能问题暂时不必考虑。
  2. 可能会暴露一些额外的信息。比如用户看到url:http://localhost/user/1,可能会猜测http://localhost/user/2http://localhost/user/1000等。
  3. JavaScript中数字是有上限的。假设你的服务要运行很久的话。
  4. 不利于分布式部署。这个暂时也不必考虑。

所以,推荐使用自动生成的唯一字符串来做id,常见的有uuid和nanoid,后者出现较晚,比uuid要短和高效,详见:mp.weixin.qq.com/s/CPSS7B7Ns…

使用上也很简单:

import {nanoid} from "https://deno.land/x/nanoid/mod.ts"
nanoid() //=> "lQLTBJKVRCuc"

2. 代码复用

假如我们要新增一个功能,比如角色的接口,你需要写个role.service.ts,功能差不多,你该怎么办?可能需要做的就是把localStorage的存储空间从users_开头修改成roles_,总不能把所有代码复制粘贴一遍吧?

程序开发中,代码复用是很重要的,一是代码结构的精减,二是利于维护,因为复制一时爽,改时火葬场,有一处逻辑修改了,你就要动两处三处甚至...

另外,UserService还有个问题是业务代码和数据处理严重耦合在一起,如果我们想要再切换localStorage存储改用数据库,是不是还得改它的代码?

这时,我们可以再加一层DAO(Data Access Object,数据访问层),专门负责增删改查等细节的API,Service层要做什么,只需要组装这些API就可以了。

分层的作用

对于初学者,可能对这些分层不了解,又或者知道有这些分层,但为什么要这样分,每层应该写什么代码一头雾水。

这里以阿里编码规范中约束的分层为例:

image.png

对应到我们现在的代码里,user.router.ts就是请求处理层,也是一些框架中常见的控制层(Controller),业务逻辑层就是现在的user.service.ts,数据持久层就是下来我们要写的model.ts,通用处理层暂时没有用到。

说下每层的作用:

  • Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。也就是说,轻业务逻辑,参数校验,异常兜底。

  • Service 层:相对具体的业务逻辑服务层。简单理解就是对下层DAO的组装调用,业务复杂的还需要事务控制。

  • Manager 层:通用业务处理层,它有如下特征:

    • 对第三方平台封装的层,预处理返回结果及转化异常信息

    • 对Service层通用能力的下沉,如缓存方案、中间件通用处理

    • 与DAO层交互,对多个DAO的组合复用

      简言之就是为了代码复用性而剥离出来的中间层,有的团队把它和Service层混在一起。

  • DAO 层:数据访问层,与底层数据库如MySQL、Oracle、MongoDB等进行数据交互。

这个称不上是业内标准,每个团队有每个团队的规则,但分层说到底还是为了明确职责边界,便于后续维护。具体怎么做可以自行摸索。这里只是推荐上述规范。

优化

在src下,新建一个model.ts的文件,内容如下:

// deno-lint-ignore-file require-await
import { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";

function getData<T = string>(key: string) {
  const str = localStorage.getItem(key);
  if (str) {
    return JSON.parse(str) as T;
  }
  return null;
}

function setData(key: string, val: unknown) {
  localStorage.setItem(key, JSON.stringify(val));
}

export class Model<T> {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  private getAllIds(): string[] {
     return getData(`${this.name}_ids`) || [];
  }

  private addToIds(id: string) {
    const ids = this.getAllIds();
    ids.push(id);
    setData(`${this.name}_ids`, ids);
    return ids.length;
  }

  /** 增加一个文档 */
  async insertOne(doc: Omit<T, "id">): Promise<string> {
    const id = nanoid();
    setData(id, { ...doc, id });
    this.addToIds(id);
    return id;
  }

  /** 查找所有 */
  async findAll(): Promise<T[]> {
    const docs = await Promise.all(
      this.getAllIds().map((id) => this.findById(id)),
    );
    return docs.filter(Boolean) as T[];
  }

  /** 根据id查找文档 */
  async findById(id: string): Promise<T | null> {
    return getData<T>(id);
  }

  /** 根据id更新文档 */
  async findByIdAndUpdate(
    id: string,
    doc: Partial<Omit<T, "id">>,
  ): Promise<{ modifiedCount: number }> {
    const oldDoc = await this.findById(id);
    const modifiedCount = 0;
    if (oldDoc) {
      for (const key in doc) {
        if (doc[key] === undefined) {
          Reflect.deleteProperty(doc, key);
        }
      }
      Object.assign(oldDoc, doc);
      setData(id, oldDoc);
    }
    return { modifiedCount };
  }

  /** 根据id删除文档 */
  async findByIdAndDelete(id: string): Promise<number> {
    localStorage.removeItem(id);
    const ids = this.getAllIds();
    const index = ids.indexOf(id);
    if (index > -1) {
      ids.splice(index, 1);
      setData(`${this.name}_ids`, ids);
      return 1;
    } else {
      console.warn(`${id} not found in ${this.name}_ids`);
      return 0;
    }
  }
}

修改user.service.ts:

// deno-lint-ignore-file require-await
import { Model } from "./model.ts";

export interface User {
  id: string;
  author: string;
  age: number;
}

class UserService {
  userModel: Model<User>;
  constructor() {
    this.userModel = new Model("users");
  }

  async getAll(): Promise<User[]> {
    return this.userModel.findAll();
  }
  async getUserById(id: string) {
    return this.userModel.findById(id);
  }

  async addUser(user: Omit<User, "id">) {
    return this.userModel.insertOne(user);
  }

  async removeUser(id: string) {
    return this.userModel.findByIdAndDelete(id);
  }

  async updateUser(id: string, user: Partial<Omit<User, "id">>) {
    return this.userModel.findByIdAndUpdate(id, user);
  }
}
export const userService = new UserService();

整段代码是不是非常干净清爽?

如果我们要新增一个role.service.ts,只需要这样:

import { Model } from "./model.ts";

class RoleService {
  roleModel: Model<Role>;
  constructor() {
    this.roleModel = new Model("roles");
  }

  async getAll(): Promise<Role[]> {
    return this.roleModel.findAll();
  }
}

注意:这时程序会报错,还需要把mod.ts中我们把id转换为数字的代码去掉。

作业

思考下,我们的文件越来越多,你该怎么调整现在的目录结构?