本文內容承接本系列的上一篇《寫給Java程序員的TypeScript入門教程(一)》。上一篇介紹了本系列教程的背景,並進行了開發環境的搭建。本系列的教學思路是經過項目實戰來學習TypeScript,選取了一個簡單的雲服務結算系統做爲實戰項目,該系統的主要功能以及代碼分層已經在上一篇中介紹過。本文內容主要介紹雲服務結算系統的domain層,具體分爲領域建模和代碼實現兩方面,在其中會穿插對TypeScript的講解。javascript
本教程教學項目的代碼都放在了github項目: typescript-tutorial-for-java-coderhtml
domain層就是所謂的領域層,在領域驅動設計中,該層主要實現了系統的一些核心業務邏輯(與具體實現無關,好比數據交互的協議、數據存儲的數據庫等)。領域建模就是對領域層的一些通用概念進行模塊設計,讓代碼可以更清晰地表達業務邏輯。領域建模不是TypeScript獨有的,它是軟件設計開發的一種方法論,是下降複雜系統理解難度的一種有效手段。領域建模可使代碼的模塊結構更加清晰,這無疑很適合TypeScript,由於TypeScript被設計出來的一個目的就是爲了改善JavaScript模塊結構混亂。java
本文會簡單介紹雲服務結算系統的領域建模過程,方便你們有更好的代入感。本系列的是TypeScript的入門教程,並不會深刻介紹領域建模相關知識。領域驅動設計是一個很好的軟件開發思想,後面會有專門的系列詳細介紹。git
在進行領域建模以前,首先須要把系統的通用語言列出來,所謂通用語言就是系統的業務邏輯經常使用的用語。列出通用語言對領域建模有很大的幫助,特別是在系統業務複雜到難如下手進行建模時。經過對一個個通用語言進行建模,分而治之,慢慢地,整個系統就清晰了。程序員
如下列出了雲服務結算系統的一些通用語言,須要特別注意的是,通用語言並非一成不變的,它會隨着項目的進程不斷調整。github
建模的過程就是把通用語言轉化爲程序語言(這裏就是TypeScript)的過程。這個過程當中,面向對象的思想很重要,只有把概念都封裝好,整個模塊的結構纔會整潔清晰。在領域驅動設計裏面有這麼幾個概念:值對象(Value Object)、實體(Entity)、領域服務(Service)、資源庫(Repository)和聚合(Aggregate)。typescript
根據這些概念的定義,咱們對前一節的通用語言進行建模,得出以下UML圖。數據庫
由於 CloudService 的結算策略是一個常常變化的方向,所以將它建模成一個接口 ChargingStrategy,本教程只提供了兩種實現:ChargingPerUsageStrategy(按需計費)和 ChargingPerPeriodStrategy(按週期計費)。另外,User 便是一個實體,也是一個聚合,購買雲服務和結算的業務邏輯都放在了 User 上。數組
domain層的實現代碼在github項目 typescript-tutorial-for-java-coder 上的src/domain/
目錄下。dom
首先看一下值對象 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類的語法和Java的很相似,好比上述代碼咱們聲明瞭一個名爲 Id 的類,它有一個私有屬性_val
、一個私有的構造函數constructor
、兩個靜態工程方法of
和random
、一個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函數變成了語言自己的一種特性,聲明時須要在函數前面加上get
或set
,調用時跟訪問類的公開成員相似。
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
來引入其餘模塊,但具體的語法和Java有細微的差異,不過這均可以經過WebStorm進行自動導入,無需過多操心。
export
爲導出的語義,與Java不一樣,在TypeScript中,若是在須要讓一個類、函數、變量在另外一個模塊/文件中可見,須要在聲明時加上export
。
// 表示其餘模塊/文件能夠引入Id這個類
export class Id {...}
複製代碼
Id 類中定義了一個私有成員_val
,其類型爲string
。在TypeScript中,string屬於基本類型,同屬的還有boolean、number、數組、元組、枚舉、any、void、null、undefined、never、Object。咱們先介紹目前爲止項目中用到的基礎類型,其他的在後續中碰到時再作詳細介紹,你們也能夠到官方文檔的中查詢全部的基礎類型。
字符串 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
類型沒有什麼意義,由於只能賦值爲null
或undefined
。
其餘值對象Telephone、Fee、Usage基本上也只用到了上述幾個基本的TypeScript特性,代碼不在本文貼出,具體實現可到github項目上查看。
本節只介紹 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函數
}
複製代碼
在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
複製代碼
**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, ...}
。後面咱們將看到,接口裏面的函數也支持這種手法進行匿名實現。
在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中數據的聲明與Java中的數據聲明類是,都是type[]
的形式,定義時稍微不一樣,TypeScript在定義數組時經過[]
將元素括起來,而Java則是使用{}
。
let list: number[] = [1, 2, 3];
複製代碼
由於在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的出現。
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函數
}
複製代碼
在上述代碼的hasBuy
函數的實現中,咱們經過將findById
的返回值與null
進行比對來判斷是否找到指定id的 CloudService 對象。
在TypeScript中,null
和undefined
也屬於基本類型,它們的值只能是null
和undefined
。默認狀況下null
和undefined
是全部類型的子類型。 就是說你能夠把 null
和undefined
賦值給number
類型的變量。可是,當指定了--strictNullChecks
標記時,null
和undefined
只能賦值給void
和它們各自。
那麼,二者又有什麼區別呢?
null表示"沒有對象",即該處不該該有值,轉爲數值時爲0;undefined表示"缺乏值",就是此處應該有一個值,可是尚未定義,轉爲數值時爲NaN。
null
的典型用法爲:
- 做爲函數的參數,表示該函數的參數不是對象。
- 做爲對象原型鏈的終點。
undefined
的典型用法爲:
- 變量被聲明瞭,但沒有賦值時,就等於undefined。
- 調用函數時,應該提供的參數沒有提供,該參數等於undefined。
- 對象沒有賦值的屬性,該屬性的值爲undefined。
- 函數沒有返回值時,默認返回undefined。
本文是《寫給Java程序員的TypeScript入門教程》系列的第二篇,主要介紹了雲服務結算系統的domain層設計與實現,包括領域建模和代碼實現。在介紹代碼實現的過程當中,穿插介紹了一些TypeScript的特性,主要包括類、接口、基礎類型這三類。TypeScript不少特性跟Java比較相似,所以做爲Java開發者,入門TypeScript相對來講難度並不大。本文只是介紹了TypeScript中一些最最基礎的特性,更多的特性須要在進行實際開發工做時經過查閱官方文檔得到。
更多深刻的內容,請關注後續的篇章。