寫給Java程式設計師的TypeScript入門教程(二)
本文內容承接本系列的上一篇《寫給Java程式設計師的TypeScript入門教程(一)》。上一篇介紹了本系列教程的背景,並進行了開發環境的搭建。本系列的教學思路是通過專案實戰來學習TypeScript,選取了一個簡單的雲服務結算系統作為實戰專案,該系統的主要功能以及程式碼分層已經在上一篇中介紹過。本文內容主要介紹雲服務結算系統的domain層,具體分為領域建模和程式碼實現兩方面,在其中會穿插對TypeScript的講解。
本教程教學專案的程式碼都放在了github專案: typescript-tutorial-for-java-coder
1 domain層領域建模
domain層就是所謂的領域層
本文會簡單介紹雲服務結算系統的領域建模過程,方便大家有更好的代入感。本系列的是TypeScript的入門教程,並不會深入介紹領域建模相關知識。領域驅動設計
是一個很好的軟體開發思想,後面會有專門的系列詳細介紹。
1.1 通用語言
在進行領域建模之前,首先需要把系統的通用語言列出來,所謂通用語言就是系統的業務邏輯常用的用語。列出通用語言對領域建模有很大的幫助,特別是在系統業務複雜到難以下手進行建模時。通過對一個個通用語言進行建模,分而治之,慢慢地,整個系統就清晰了。
以下列出了雲服務結算系統的一些通用語言,需要特別注意的是,通用語言並不是一成不變的,它會隨著專案的程式不斷調整。
1.2 建模
建模的過程就是把通用語言轉化為程式語言(這裡就是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/
目錄下。
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
、兩個靜態工程方法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和export
TypeScript也是通過import
來引入其他模組,但具體的語法和Java有細微的差別,不過這都可以通過WebStorm進行自動匯入,無需過多操心。
export
為匯出的語義,與Java不同,在TypeScript中,如果在需要讓一個類、函式、變數在另一個模組/檔案中可見,需要在宣告時加上export
。
// 表示其他模組/檔案可以引入Id這個類
export class Id {...}
複製程式碼
TypeScript特性——基礎型別 string、number、boolean、void
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專案上檢視。
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,};
複製程式碼
業務程式碼中並不推薦這樣實現,這正是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中,null
和undefined
也屬於基本型別,它們的值只能是null
和undefined
。預設情況下null
和undefined
是所有型別的子型別。 就是說你可以把 null
和undefined
賦值給number
型別的變數。但是,當指定了--strictNullChecks
標記時,null
和undefined
只能賦值給void
和它們各自。
那麼,兩者又有什麼區別呢?
null表示"沒有物件",即該處不應該有值,轉為數值時為0;undefined表示"缺少值",就是此處應該有一個值,但是還沒有定義,轉為數值時為NaN。
null
的典型用法為:
- 作為函式的引數,表示該函式的引數不是物件。
- 作為物件原型鏈的終點。
undefined
的典型用法為:
- 變數被宣告瞭,但沒有賦值時,就等於undefined。
- 呼叫函式時,應該提供的引數沒有提供,該引數等於undefined。
- 物件沒有賦值的屬性,該屬性的值為undefined。
- 函式沒有返回值時,預設返回undefined。
3 總結
本文是《寫給Java程式設計師的TypeScript入門教程》系列的第二篇,主要介紹了雲服務結算系統的domain層設計與實現,包括領域建模和程式碼實現。在介紹程式碼實現的過程中,穿插介紹了一些TypeScript的特性,主要包括類、介面、基礎型別這三類。TypeScript很多特性跟Java比較類似,因此作為Java開發者,入門TypeScript相對來說難度並不大。本文只是介紹了TypeScript中一些最最基礎的特性,更多的特性需要在進行實際開發工作時通過查閱官方檔案獲得。
更多深入的內容,請關注後續的篇章。