[譯] 模式 — 使用 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索