[译] 模式 — 使用 Typescript 和 Node.js 的通用仓储

成为代码之王

若是你使用 Node.js/Javascript,而且有不少应付不一样数据模型的重复代码或者不厌其烦地建立 CRUD(Create, Read, Update and Delete),那么这篇文章适合你!javascript


通用仓储模式

在写 Javascript 应用的时候,咱们存在在不一样应用中共享类似代码的问题,而且有些时候,咱们为不一样的应用写相同的代码。当咱们有一个(或更多)抽象类,并重用与数据模型解耦的实现时,这种模式赋予你编写数据抽象的能力,只需为某些类传入类型。html

谈到 仓储模式,它指当你须要对数据库进行操做时,你能够将全部的数据库操做(Create, Read, Update 和 Delete 操做)对保存在每一个本地惟一的业务实体中,而不是直接调用数据库驱动。若是你有多于一个数据库,或者一个事务涉及到多个数据库,你的应用应当只调用仓储中的方法,那么谁调用了方法也显而易见。前端

所以,通用仓储 与之相似,不一样的是,如今你只有一个抽象,一个具备全部常见操做的基类。而你的 实体仓储仅拓展基类以及基类中全部的数据库操做实现。遵循 SOLID 原则,该模式遵循 开放/封闭 原则,你的基类对拓展开放,而对于修改是关闭的。java

什么时候使用通用仓储?

取决于你的业务类型和应用程序的关键级别。我认为这种模式的具备可拓展性。当你用用程序的全部实体都要有 CURD 或者相似操做的时候,它可让你只须要建立一个类来编写全部常见操做,诸如 CURDnode

何时不要使用通用仓储?

与拥有的能力相同,你也会有危险的隐含代码(不要使用通用仓储),一个简单的例子就是:android

  • 你有两个实体类:PeopleAccountios

  • 用户能够删除 Peoplegit

  • 用户没法更新 Account 的相关信息(例如向帐户增长更多的钱)程序员

  • 若是两个类都拓展自具备 update()remove() 方法的基类,那么程序员必须谨记那一点,而且不要把 remove 或者 update 方法暴露给服务,负责你的业务案例将会是危险并错误的。github

Typescript 的泛型

可以处理当前乃至将来数据的组件将为你提供构建大型软件系统的最灵活的功能 —— typescriptlang.org/docs/handbo…

遵循 Typescript 的文档,泛型提供了构建灵活和通用组件(或类型)的能力,从他们的文档中,咱们有一个更好的例子来讲明它如何工做:

function identity(arg: number): number {
    return arg;
}
复制代码

因此,咱们有一个成熟的方法,他接收一个数字并返回相同类型。若是要将一个字符串传递给此方法,则须要使用相同的实现建立另外一个方法并重复代码。

经过泛型实现,咱们用一个明确的词来讲明什么是泛型实现(约定,使用 T 来表示它是泛型类型)

function identity<T>(arg: T): T {
  return arg;
}

// call
const result = identity<string>('Erick Wendel');
console.log('string is', result);

const resultNumber = identity<number>(200);
console.log('number is ', resultNumber);

/** * string is Erick Wendel number is 200 */
复制代码

使用通用仓储和 Node.js 来建立一个真实的项目

Lets go! 若是你尚未理解(译者注:这里本来的词是 understated,应该是 understand?),经过下一部分的学习你应该就会理解了。

要求:

测试你的环境

安装完全部的环境要求以后,若是一切正常,请在 terminal 中运行测试。

npm --v && node --version
复制代码

Output of commands to view Node.js version and npm versions

要验证 MongoDB 是否正常,请在另外一个 terminal tab 上运行,sudo mongod

MongoDB Instance Starting

而后,另外一个 tab 上运行 mongo 以进入你的数据库。

Entering em MongoDB database

而后,全局安装 typescript,以编译你的 typescript 项目。运行 npm install -g typescript

Output of typescript globally package installed

一旦你已经完成,咱们就能够继续前进 :D


如今,咱们须要建立一个文件夹而且初始化一个 Node.js 项目。

mkdir warriors-project
cd warriors-pŕoject
npm init -y #to init nodejs app without wizard
tsc --init  #to init config file to typescript
复制代码

以后,应该在 vscode 中打开你的项目文件夹。要建立咱们的项目,你得建立一些文件夹以便更好地构建咱们的应用程序。咱们将使用如下的文件夹结构:

.
├── entities 
├── package.json
├── repositories
│ ├── base 
│ └── interfaces 
└── tsconfig.json
复制代码

进入 tsconfig.json 文件,将属性 "lib": [] 部分值修改成 "lib": [ "es2015"],咱们改变 json 文件的属性,以使用 es2015 模块,例如 Typescript 中的 Promises。将 outDir属性修改成 "outDir": "lib" 以便在另外一个文件夹中生成 .js 文件。

关于咱们的文件夹,entities 文件夹是存放你的数据模型,repositories 文件夹关于数据库操做,interfaces 是咱们操做的合同(contracts)。如今,咱们应该在 entities 文件夹中建立咱们的实体,使用如下代码建立 Spartan.ts 文件

export class Spartan {
  private name: string;
  private kills: number;

  constructor(name: string, kills: number) {
    this.name = name;
    this.kills = kills;
  }
}
复制代码

如今,在 repositories/interfaces 文件夹,咱们将建立两个文件, 遵循 单一功能(Single responsibility) 这些文件将具备抽象类必须有的合同。咱们的合同应该遵循通用模式,能够在没有固定类型的状况下编写,可是,当任何人实现此接口时,应该为它们传递类型。

export interface IWrite<T> {
  create(item: T): Promise<boolean>;
  update(id: string, item: T): Promise<boolean>;
  delete(id: string): Promise<boolean>;
}
复制代码
export interface IRead<T> {
  find(item: T): Promise<T[]>;
  findOne(id: string): Promise<T>;
}
复制代码

在建立接口以后,咱们应该建立基类,这是一个实现全部通用接口的抽象类,而且具备咱们对全部实体的通用实现。在 base 文件夹中,咱们使用下面的代码建立 BaseRepository.ts

Creating BaseRepository with Interfaces imported

导入接口(interface)以后,须要实现接口的签名。为此能够按 ctrl . 显示 vscode 的选项来修复有问题的地方。而后单击 “Implements Interface IWrite<T> (Fix all in file)” 来添加全部实现.

After open options and select fix all in files

如今咱们有一个相似下面代码的类

// import all interfaces
import { IWrite } from '../interfaces/IWrite';
import { IRead } from '../interfaces/IRead';

// that class only can be extended
export abstract class BaseRepository<T> implements IWrite<T>, IRead<T> {
    create(item: T): Promise<boolean> {
        throw new Error("Method not implemented.");
    }
    update(id: string, item: T): Promise<boolean> {
        throw new Error("Method not implemented.");
    }
    delete(id: string): Promise<boolean> {
        throw new Error("Method not implemented.");
    }
    find(item: T): Promise<T[]> {
        throw new Error("Method not implemented.");
    }
    findOne(id: string): Promise<T> {
        throw new Error("Method not implemented.");
    }
}
复制代码

咱们如今应该为全部的方法建立实现。BaseRepository 类应该知道如何访问你可以使用的数据库和集合。此时,你须要安装 Mongodb 驱动包。因此你须要返回到 terminal 中的项目文件夹,运行 npm i -S mongodb @types/mongodb 添加 mongodb 驱动和 typescript 的定义包。

constructor 中,咱们添加两个参数,dbcollectionName。类的实现应该和下面的代码差很少

// import all interfaces
import { IWrite } from '../interfaces/IWrite';
import { IRead } from '../interfaces/IRead';

// we imported all types from mongodb driver, to use in code
import { MongoClient, Db, Collection, InsertOneWriteOpResult } from 'mongodb';

// that class only can be extended
export abstract class BaseRepository<T> implements IWrite<T>, IRead<T> {
  //creating a property to use your code in all instances 
  // that extends your base repository and reuse on methods of class
  public readonly _collection: Collection;

  //we created constructor with arguments to manipulate mongodb operations
  constructor(db: Db, collectionName: string) {
    this._collection = db.collection(collectionName);
  }

  // we add to method, the async keyword to manipulate the insert result
  // of method.
  async create(item: T): Promise<boolean> {
    const result: InsertOneWriteOpResult = await this._collection.insert(item);
    // after the insert operations, we returns only ok property (that haves a 1 or 0 results)
    // and we convert to boolean result (0 false, 1 true)
    return !!result.result.ok;
  }


  update(id: string, item: T): Promise<boolean> {
    throw new Error('Method not implemented.');
  }
  delete(id: string): Promise<boolean> {
    throw new Error('Method not implemented.');
  }
  find(item: T): Promise<T[]> {
    throw new Error('Method not implemented.');
  }
  findOne(id: string): Promise<T> {
    throw new Error('Method not implemented.');
  }
}
复制代码

如今,咱们在 repositories 文件夹中为特定实体建立了 Repository 文件。

import { BaseRepository } from "./base/BaseRepository";
import { Spartan } from "../entities/Spartan"

// now, we have all code implementation from BaseRepository
export class SpartanRepository extends BaseRepository<Spartan>{

    // here, we can create all especific stuffs of Spartan Repository
    countOfSpartans(): Promise<number> {
        return this._collection.count({})
    }
}
复制代码

如今,去测试仓储和全部的逻辑事件。咱们须要在项目根路径下建立一个 Index.ts 文件,来调用全部的仓储。

// importing mongoClient to connect at mongodb
import { MongoClient } from 'mongodb';

import { SpartanRepository } from './repositories/SpartanRepository'
import { Spartan } from './entities/Spartan';


// creating a function that execute self runs
(async () => {
    // connecting at mongoClient
    const connection = await MongoClient.connect('mongodb://localhost');
    const db = connection.db('warriors');

    // our operations
    // creating a spartan
    const spartan = new Spartan('Leonidas', 1020);

    // initializing the repository
    const repository = new SpartanRepository(db, 'spartans');

    // call create method from generic repository
    const result = await repository.create(spartan);
    console.log(`spartan inserted with ${result ? 'success' : 'fail'}`)

    //call specific method from spartan class
    const count = await repository.countOfSpartans();
    console.log(`the count of spartans is ${count}`)

    /** * spartan inserted with success the count of spartans is 1 */
})();
复制代码

你须要将你的 Typescript 转换成 Javascript 文件, 在 terminal 中运行 tsc 命令。如今 lib 文件夹中你拥有了所有的 javascript 文件,如此这般,你能够经过 node lib/Index.js. 运行你的程序。

为了让你领略到通用仓储的强大之处,咱们将为名为 HeroesRepository.tsHeroes,以及一个实体类建立更多的仓储,这表明一位 Hero

// entities/Hero.ts

export class Hero {
    private name: string;
    private savedLifes: number;

    constructor(name: string, savedLifes: number) {
        this.name = name;
        this.savedLifes = savedLifes;
    }
}
复制代码
// repositories/HeroRepository.ts

import { BaseRepository } from "./base/BaseRepository";
import { Hero } from "../entities/Hero"

export class HeroRepository extends BaseRepository<Hero>{

}
复制代码

如今,咱们只须要在 Index.ts 中调用仓储,下面是完整代码。

// importing mongoClient to connect at mongodb
import { MongoClient } from 'mongodb';

import { SpartanRepository } from './repositories/SpartanRepository'
import { Spartan } from './entities/Spartan';

//importing Hero classes
import { HeroRepository } from './repositories/HeroRepository'
import { Hero } from './entities/Hero';

// creating a function that execute self runs
(async () => {
    // connecting at mongoClient
    const connection = await MongoClient.connect('mongodb://localhost');
    const db = connection.db('warriors');

    // our operations
    // creating a spartan
    const spartan = new Spartan('Leonidas', 1020);

    // initializing the repository
    const repository = new SpartanRepository(db, 'spartans');

    // call create method from generic repository
    const result = await repository.create(spartan);
    console.log(`spartan inserted with ${result ? 'success' : 'fail'}`)

    //call specific method from spartan class
    const count = await repository.countOfSpartans();
    console.log(`the count of spartans is ${count}`)

    /** * spartan inserted with success the count of spartans is 1 */

    const hero = new Hero('Spider Man', 200);
    const repositoryHero = new HeroRepository(db, 'heroes');
    const resultHero = await repositoryHero.create(hero);
    console.log(`hero inserted with ${result ? 'success' : 'fail'}`)
    
})();
复制代码

总结

对于一个类,咱们有不少实现能够采用而且让工做更容易。对于我来讲,TypeScript 中的泛型功能是最强大的功能之一。你在此处看到的全部代码均可以在 GitHub 的 repo 中找到。你能够在下面的连接中找出它们,不要忘记查看 :D

若是你到了这儿,不要吝啬你的评论,分享给你的朋友并留下反馈。固然这是个人第一篇英文帖子,若是你碰巧发现任何错误,请经过私信纠正我 :D

不要忘了点赞哦!


Links

See ya 🤘

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索