1. 程式人生 > 程式設計 >寫給Java程式設計師的TypeScript入門教程(二)

寫給Java程式設計師的TypeScript入門教程(二)

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

本教程教學專案的程式碼都放在了github專案: typescript-tutorial-for-java-coder

1 domain層領域建模

domain層就是所謂的領域層

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

本文會簡單介紹雲服務結算系統的領域建模過程,方便大家有更好的代入感。本系列的是TypeScript的入門教程,並不會深入介紹領域建模相關知識。領域驅動設計

是一個很好的軟體開發思想,後面會有專門的系列詳細介紹。

1.1 通用語言

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

以下列出了雲服務結算系統的一些通用語言,需要特別注意的是,通用語言並不是一成不變的,它會隨著專案的程式不斷調整。

1.2 建模

建模的過程就是把通用語言轉化為程式語言(這裡就是TypeScript)的過程。這個過程中,面向物件的思想很重要,只有把概念都封裝好,整個模組的結構才會整潔清晰。在領域驅動設計

裡面有這麼幾個概念:值物件(Value Object)、實體(Entity)、領域服務(Service)、資源庫(Repository)和聚合(Aggregate)。

  • 值物件:一些沒有唯一標識的簡單物件,常常是不可變的,如果需要修改就整個物件替換掉,如電話。
  • 實體:在整個系統中具有唯一標識的物件,如使用者。
  • 領域服務:當系統中一些業務邏輯不適合放在值物件或實體中時,就可以建模為領域服務。
  • 資源庫:用於值物件或實體的持久化儲存,在領域層中往往是一個抽象介面,具體實現放在基礎設施層。
  • 聚合:領域物件的組合,用於封裝業務,並保證聚合內領域物件的資料一致性。

根據這些概念的定義,我們對前一節的通用語言進行建模,得出如下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、兩個靜態工程方法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,};
複製程式碼

業務程式碼中並不推薦這樣實現,這正是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中一些最最基礎的特性,更多的特性需要在進行實際開發工作時通過查閱官方檔案獲得。

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