寫給Java程序員的TypeScript入門教程(二)

本文內容承接本系列的上一篇《寫給Java程序員的TypeScript入門教程(一)》。上一篇介紹了本系列教程的背景,並進行了開發環境的搭建。本系列的教學思路是經過項目實戰來學習TypeScript,選取了一個簡單的雲服務結算系統做爲實戰項目,該系統的主要功能以及代碼分層已經在上一篇中介紹過。本文內容主要介紹雲服務結算系統domain層,具體分爲領域建模代碼實現兩方面,在其中會穿插對TypeScript的講解。javascript

本教程教學項目的代碼都放在了github項目: typescript-tutorial-for-java-coderhtml

1 domain層領域建模

domain層就是所謂的領域層,在領域驅動設計中,該層主要實現了系統的一些核心業務邏輯(與具體實現無關,好比數據交互的協議、數據存儲的數據庫等)。領域建模就是對領域層的一些通用概念進行模塊設計,讓代碼可以更清晰地表達業務邏輯。領域建模不是TypeScript獨有的,它是軟件設計開發的一種方法論,是下降複雜系統理解難度的一種有效手段。領域建模可使代碼的模塊結構更加清晰,這無疑很適合TypeScript,由於TypeScript被設計出來的一個目的就是爲了改善JavaScript模塊結構混亂。java

本文會簡單介紹雲服務結算系統的領域建模過程,方便你們有更好的代入感。本系列的是TypeScript的入門教程,並不會深刻介紹領域建模相關知識。領域驅動設計是一個很好的軟件開發思想,後面會有專門的系列詳細介紹。git

1.1 通用語言

在進行領域建模以前,首先須要把系統的通用語言列出來,所謂通用語言就是系統的業務邏輯經常使用的用語。列出通用語言對領域建模有很大的幫助,特別是在系統業務複雜到難如下手進行建模時。經過對一個個通用語言進行建模,分而治之,慢慢地,整個系統就清晰了。程序員

如下列出了雲服務結算系統的一些通用語言,須要特別注意的是,通用語言並非一成不變的,它會隨着項目的進程不斷調整。github

1.2 建模

建模的過程就是把通用語言轉化爲程序語言(這裏就是TypeScript)的過程。這個過程當中,面向對象的思想很重要,只有把概念都封裝好,整個模塊的結構纔會整潔清晰。在領域驅動設計裏面有這麼幾個概念:值對象(Value Object)、實體(Entity)、領域服務(Service)、資源庫(Repository)和聚合(Aggregate)。typescript

  • 值對象:一些沒有惟一標識的簡單對象,經常是不可變的,若是須要修改就整個對象替換掉,如電話。
  • 實體:在整個系統中具備惟一標識的對象,如用戶。
  • 領域服務:當系統中一些業務邏輯不適合放在值對象或實體中時,就能夠建模爲領域服務。
  • 資源庫:用於值對象或實體的持久化存儲,在領域層中每每是一個抽象接口,具體實現放在基礎設施層。
  • 聚合:領域對象的組合,用於封裝業務,並保證聚合內領域對象的數據一致性。

根據這些概念的定義,咱們對前一節的通用語言進行建模,得出以下UML圖。數據庫

  • 值對象:Id(惟一標識符)、Telephone(聯繫電話)、Fee(金額)、Usage(資源使用量)
  • 實體:User(用戶)、CloudService(雲服務)
  • 資源庫:UserRepository(用戶資源庫)、CloudServiceRepository(雲服務資源庫)
  • 聚合:User(用戶)

由於 CloudService 的結算策略是一個常常變化的方向,所以將它建模成一個接口 ChargingStrategy,本教程只提供了兩種實現:ChargingPerUsageStrategy(按需計費)和 ChargingPerPeriodStrategy(按週期計費)。另外,User 便是一個實體,也是一個聚合,購買雲服務和結算的業務邏輯都放在了 User 上。數組

2 domain層實現

domain層的實現代碼在github項目 typescript-tutorial-for-java-coder 上的src/domain/目錄下。dom

2.1 值對象

首先看一下值對象 Id 的具體實現:

// src/domain/id.ts
import {v1 as uuid} from 'uuid';

// 惟一標識ID值對象
export class Id {
  private readonly _val: string;

  private constructor(val: string) {
    this._val = val;
  }
  // 工廠方法
  static of(val: string): Id {
    return new Id(val);
  }
  // 返回一個隨機的ID,值爲UUID
  static random(): Id {
    const val = uuid();
    return new Id(val);
  }

  get val(): string {
    return this._val;
  }
}
複製代碼

TypeScript特性——類

TypeScript類的語法和Java的很相似,好比上述代碼咱們聲明瞭一個名爲 Id 的類,它有一個私有屬性_val、一個私有的構造函數constructor、兩個靜態工程方法ofrandom、一個get方法val

TypeScript的類有三種修飾符,分別是公開public、私有private和受保護protected,當類中的成員不指定修飾符時,默認爲public

與TypeScript相比,Java類的成員若是不指定修飾符,默認爲包內可見。

TypeScript與Java有一個很明顯的區別就是,變量/返回值的類型聲明是跟在變量/函數的後面的。如private readonly _val: string的意思是聲明一個私有的不可變成員_val,類型爲string。值得注意的是,在類中聲明一個成員爲不可變時須要使用readonly來進行限定

TypeScript中的構造函數統一使用constructor進行聲明,而不是使用類名,這一點與Java有着明顯的不一樣。與Java相似,TypeScript中也有靜態成員,使用static進行限定,訪問時經過類名進行訪問。

const id = Id.of('test-id');
expect(id.val).toEqual('test-id');
複製代碼

使用靜態工廠方法來建立實例可讓代碼可讀性更好,並且讓建立對象的邏輯與對象使用者解耦。好比後續若是須要把類改爲單例模式,只需修改靜態工廠方法實現便可,對象的使用者無需作任何變更。

Java程序員必定對getter/setter函數不陌生,在TypeScript裏,getter/setter函數變成了語言自己的一種特性,聲明時須要在函數前面加上getset,調用時跟訪問類的公開成員相似。

class Id {
  ...
  // 聲明get函數
  get val(): string {
    return this._val;
  }
  // 聲明set函數
  get val(newVal: string): void {
    this._val = newVal;
  }
  ...
}
// 使用例子
const tmpVal = id.val; // 調用get val()函數
id.val = '12345'       // 調用set val(newVal: string)函數
複製代碼

TypeScript特性——import和export

TypeScript也是經過import來引入其餘模塊,但具體的語法和Java有細微的差異,不過這均可以經過WebStorm進行自動導入,無需過多操心。

export爲導出的語義,與Java不一樣,在TypeScript中,若是在須要讓一個類、函數、變量在另外一個模塊/文件中可見,須要在聲明時加上export

// 表示其餘模塊/文件能夠引入Id這個類
export class Id {...}
複製代碼

TypeScript特性——基礎類型 string、number、boolean、void

Id 類中定義了一個私有成員_val,其類型爲string。在TypeScript中,string屬於基本類型,同屬的還有booleannumber數組元組枚舉anyvoidnullundefinedneverObject。咱們先介紹目前爲止項目中用到的基礎類型,其他的在後續中碰到時再作詳細介紹,你們也能夠到官方文檔的中查詢全部的基礎類型

字符串 string

TypeScript可使用雙引號( ")或單引號(')表示字符串。string有個很好的特性——模板字符串,這種字符串是被反引號包圍(` ),而且以${ expr }這種形式嵌入表達式。

let name: string = 'Gene';
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }. I'll be ${ age + 1 } years old next month.`;
// sentence的值爲 Hello, my name is Gene. I'll be 38 years old next month.
複製代碼

Java中使用雙引號表示字符串類型String,單引號表示字符類型char。

數字 number

TypeScript再也不區分int、long、double等這些數字類型,全部的數字都屬於浮點數類型number

布爾值 boolean

TypeScript中的布爾值類型boolean與Java中的定義同樣,包含true/false兩種值。

void

與Java相似,void表示沒有任何類型,當一個函數沒有返回值時,其返回類型就是void而聲明一個變量爲void類型沒有什麼意義,由於只能賦值爲nullundefined

其餘值對象TelephoneFeeUsage基本上也只用到了上述幾個基本的TypeScript特性,代碼不在本文貼出,具體實現可到github項目上查看。

2.2 實體

本節只介紹 CloudService 實體,User 實體放到聚合實現一節介紹,CloudService的實現以下:

// src/domain/cloud-service.ts
// 雲服務 實體
export class CloudService {
  // 用戶購買的雲服務惟一標識
  private readonly _id: Id;
  // 雲服務名
  private readonly _name: string;
  // 雲服務的結算策略
  private readonly _chargingStrategy: ChargingStrategy;
  
  ... // 私有構造函數
  
  // 靜態工廠方法
  static of(name: string, chargingStrategy: ChargingStrategy, id?: Id): CloudService {
    // 若是沒有傳入Id值,則賦值UUID
    if (!id) {
      id = Id.random();
    }
    return new CloudService(name, chargingStrategy, id);
  }
  // 對資源使用量進行結算結算
  charging(usage: Usage): Fee {
    return this._chargingStrategy.charging(usage);
  }

	... // get、set函數
}

複製代碼

TypeScript特性——函數的可選參數

在CloudService的靜態工廠方法的入參id後面跟了一個?,這個是TypeScript函數的可選參數用法。當調用者沒有傳遞id這個參數時,id的值爲undefined

// 指定Id
let cloudService = CloudService.of('HBase', strategy, Id.of('123'));
expect(cloudService.id.val).toEqual('123');
// 不指定Id
cloudService = CloudService.of('HBase', strategy);
console.log(cloudService.id.val) // 輸出一個UUID
複製代碼

TypeScript特性——接口

**CloudService ** 的私有屬性_chargingStrategy的類型是 ChargingStrategy,它是一個接口,其定義和實現類以下:

// src/domain/charging-strategy.interface.ts
// 結算策略抽象接口
export interface ChargingStrategy {
  /** * 對雲服務的使用量進行計費. * @param usage 雲服務對應對資源使用量 * @return 需付金額 */
  charging(usage: Usage): Fee;
}

// src/domain/charging-per-usage-strategy.ts
// 按需計費策略,實現ChargingStrategy接口
export class ChargingPerUsageStrategy implements ChargingStrategy {
  // 單價
  private readonly _price: number;
  
  ... // 構造函數與靜態工程方法
  
  charging(usage: Usage): Fee {
    // 單價*使用量
    const fee = this._price * usage.val;
    return Fee.of(fee);
  }
}
複製代碼

從這個例子看,TypeScript中的接口與Java中的接口很相似,使用interface進行聲明,接口中的函數只聲明,具體實現放到實現類上。

除了函數以外,TypeScript還支持在接口中聲明屬性,這是Java接口所不支持的。

// 接口SquareConfig聲明瞭兩個屬性
export interface SquareConfig {
  color: string;
  width: number;
}
// 實現接口
let config: SquareConfig = {color: 'red', width: 50};
expect(config.color).toEqual('red');
expect(config.width).toEqual(50);
複製代碼

上述例子中,接口的實現並無像 ChargingPerUsageStrategy 這樣建立一個子類,而是採用了相似Java裏面經過lambda表達式匿名實現接口的手法:{field1: implementation, ...}。後面咱們將看到,接口裏面的函數也支持這種手法進行匿名實現。

2.3 資源庫

在domain層中,資源庫(Repository)只給出接口,不提供具體實現。由於領域層應該只關係系統的業務邏輯,至於一些涉及到具體實現(如數據庫持久化)的代碼應該放到基礎設施層上。

本節值介紹 CloudServiceRepository 的定義,UserServiceRepository 的定義與CloudServiceRepository相似,具體能夠到github項目上查看。

// src/domain/cloud-service-repository.ts
export interface CloudServiceRepository {
  // 保存雲服務
  save(cloudService: CloudService, userId: Id): boolean;
  // 刪除雲服務
  delete(cloudService: CloudService): boolean;
  // 根據雲服務ID查找
  findById(cloudServiceId: Id): CloudService;
  // 根據用戶ID查找
  findByUserId(userId: Id): CloudService[];
}
複製代碼

TypeScript特性——基礎類型 數組

TypeScript中數據的聲明與Java中的數據聲明類是,都是type[]的形式,定義時稍微不一樣,TypeScript在定義數組時經過[]將元素括起來,而Java則是使用{}

let list: number[] = [1, 2, 3];
複製代碼

TypeScript特性——接口 匿名實現

由於在domain層中資源庫沒有具體實現,在進行單元測試時,依賴了資源庫的類要怎麼測試呢?這時就能夠採用前面提到的匿名實現手法。

const repository: CloudServiceRepository = {
  save: (cloudService, userId) => true,
  delete: (cloudService) => true,
  findById: (serviceId) => null,
  findByUserId: (userId) => [],
};
複製代碼

能夠看到,函數的匿名實現很像Java裏面的lambda表達式,在TypeScript裏面,箭頭再也不是->,而是=>

此外,還能夠只實現部分函數,只需在前一行加上@ts-ignore,這樣就能夠減小單元測試的多餘實現了。

// @ts-ignore
const repository: CloudServiceRepository = {
  save: (cloudService, userId) => true,
};
複製代碼

業務代碼中並不推薦這樣實現,這正是TypeScript相對JavaScript有所改進的地方,增長了靜態檢查,減小Bug的出現。

2.4 聚合

User 便是一個實體,也是一個聚合,實現了購買雲服務和結算的業務邏輯。

export class User {
  // 用戶惟一標識
  private readonly _id: Id;
  // 用戶名
  private readonly _name: string;
  // 用戶聯繫方式
  private readonly _phone: Telephone;
  // 雲服務倉庫
  private readonly _serviceRepository: CloudServiceRepository;
  
  ... // 構造函數和靜態工廠方法
  
  // 購買雲服務.
  buy(cloudService: CloudService): boolean {
    return this._serviceRepository.save(cloudService, this._id);
  }

  // 判斷用戶是否已經購買了這個雲服務.
  hasBuy(serviceId: Id): boolean {
    return this._serviceRepository.findById(serviceId) != null;
  }

  // 對雲服務使用量進行結算.
  settle(service: CloudService, usage: Usage): Fee {
    return service.charging(usage);
  }
 
  ... // get、set函數
}
複製代碼

TypeScript特性——基礎類型 null、undefined

在上述代碼的hasBuy函數的實現中,咱們經過將findById的返回值與null進行比對來判斷是否找到指定id的 CloudService 對象。

在TypeScript中,nullundefined也屬於基本類型,它們的值只能是nullundefined。默認狀況下nullundefined是全部類型的子類型。 就是說你能夠把 nullundefined賦值給number類型的變量。可是,當指定了--strictNullChecks標記時,nullundefined只能賦值給void和它們各自。

那麼,二者又有什麼區別呢?

null表示"沒有對象",即該處不該該有值,轉爲數值時爲0;undefined表示"缺乏值",就是此處應該有一個值,可是尚未定義,轉爲數值時爲NaN。

null的典型用法爲:

  1. 做爲函數的參數,表示該函數的參數不是對象。
  2. 做爲對象原型鏈的終點。

undefined的典型用法爲:

  1. 變量被聲明瞭,但沒有賦值時,就等於undefined。
  2. 調用函數時,應該提供的參數沒有提供,該參數等於undefined。
  3. 對象沒有賦值的屬性,該屬性的值爲undefined。
  4. 函數沒有返回值時,默認返回undefined。

3 總結

本文是《寫給Java程序員的TypeScript入門教程》系列的第二篇,主要介紹了雲服務結算系統的domain層設計與實現,包括領域建模代碼實現。在介紹代碼實現的過程當中,穿插介紹了一些TypeScript的特性,主要包括類、接口、基礎類型這三類。TypeScript不少特性跟Java比較相似,所以做爲Java開發者,入門TypeScript相對來講難度並不大。本文只是介紹了TypeScript中一些最最基礎的特性,更多的特性須要在進行實際開發工做時經過查閱官方文檔得到。

更多深刻的內容,請關注後續的篇章。

相關文章
相關標籤/搜索