转载

写给Java程序员的TypeScript入门教程(二)

本文内容承接本系列的上一篇 《写给Java程序员的TypeScript入门教程(一)》 。上一篇介绍了本系列教程的背景,并进行了开发环境的搭建。本系列的教学思路是通过项目实战来学习TypeScript,选取了一个简单的 云服务结算系统 作为实战项目,该系统的主要功能以及代码分层已经在上一篇中介绍过。本文内容主要介绍 云服务结算系统domain 层,具体分为 领域建模代码实现 两方面,在其中会穿插对TypeScript的讲解。

本教程教学项目的代码都放在了github项目: typescript-tutorial-for-java-coder

1 domain层领域建模

domain层就是所谓的 领域层 ,在 领域驱动设计 中,该层主要实现了系统的一些核心业务逻辑(与具体实现无关,比如数据交互的协议、数据存储的数据库等)。 领域建模 就是对领域层的一些通用概念进行模块设计,让代码能够更清晰地表达业务逻辑。领域建模不是TypeScript独有的,它是软件设计开发的一种方法论,是降低复杂系统理解难度的一种有效手段。领域建模可以使代码的模块结构更加清晰,这无疑很适合TypeScript,因为TypeScript被设计出来的一个目的就是为了改善JavaScript模块结构混乱。

本文会简单介绍 云服务结算系统 的领域建模过程,方便大家有更好的代入感。本系列的是TypeScript的入门教程,并不会深入介绍领域建模相关知识。 领域驱动设计 是一个很好的软件开发思想,后面会有专门的系列详细介绍。

1.1 通用语言

在进行领域建模之前,首先需要把系统的 通用语言 列出来,所谓 通用语言 就是系统的业务逻辑常用的用语。列出通用语言对领域建模有很大的帮助,特别是在系统业务复杂到难以下手进行建模时。通过对一个个通用语言进行建模,分而治之,慢慢地,整个系统就清晰了。

以下列出了 云服务结算系统 的一些通用语言,需要特别注意的是,通用语言并不是一成不变的,它会随着项目的进程不断调整。

写给Java程序员的TypeScript入门教程(二)

1.2 建模

建模的过程就是把通用语言转化为程序语言(这里就是TypeScript)的过程。这个过程中, 面向对象的思想 很重要,只有把概念都封装好,整个模块的结构才会整洁清晰。在 领域驱动设计 里面有这么几个概念: 值对象 (Value Object)、 实体 (Entity)、 领域服务 (Service)、 资源库 (Repository)和 聚合 (Aggregate)。

  • 值对象 :一些没有唯一标识的简单对象,常常是不可变的,如果需要修改就整个对象替换掉,如电话。
  • 实体 :在整个系统中具有唯一标识的对象,如用户。
  • 领域服务 :当系统中一些业务逻辑不适合放在值对象或实体中时,就可以建模为领域服务。
  • 资源库 :用于值对象或实体的持久化存储,在领域层中往往是一个抽象接口,具体实现放在基础设施层。
  • 聚合 :领域对象的组合,用于封装业务,并保证聚合内领域对象的数据一致性。

根据这些概念的定义,我们对前一节的通用语言进行建模,得出如下UML图。

写给Java程序员的TypeScript入门教程(二)
  • 值对象: 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, 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中一些最最基础的特性,更多的特性需要在进行实际开发工作时通过查阅官方文档获得。

更多深入的内容,请关注后续的篇章。

写给Java程序员的TypeScript入门教程(二)
原文  https://juejin.im/post/5dd104b2518825316f28255f
正文到此结束
Loading...