论TypeScript中的错误处理表示与领域驱动设计的优势
const sumPatientCounts = (...patientCounts: Array<number>): number =>
Math.sum(...patientCounts);
const sumPatientCounts = (...patientCounts: Array<number>): number => {
if (patientCounts.some(patientCount => patientCount < 0)) {
throw new Error('All patient counts should be striclty positive');
}
return Math.sum(...patientCounts);
};
const sumPatientCounts = (...patientCounts: Array<number>): number => {
if (patientCounts.some(patientCount => patientCount < 0)) {
throw new Error('All patient counts should be striclty positive');
}
return Math.sum(...patientCounts);
};
const sumPatientCounts = (...patientCounts: Array<number>): number | null => {
if (patientCounts.some(patientCount => patientCount < 0)) {
return null;
}
return Math.sum(...patientCounts);
};
const globalPatientCount = sumPatientCounts(1, 2);
if (globalPatientCount == null) {
// do something in that case
}
return computeSomething(globalPatientCount);
我们希望方法签名能够明确表示引发错误状态的可能性
我们希望每种方法都能支持任意多种错误类型
除非必要,否则我们不希望破坏方法调用流程
export type Either<L, A> = Left<L, A> | Right<L, A>;
export class Left<L, A> {
readonly value: L;
constructor(value: L) {
this.value = value;
}
isLeft(): this is Left<L, A> {
return true;
}
isRight(): this is Right<L, A> {
return false;
}
}
export class Right<L, A> {
readonly value: A;
constructor(value: A) {
this.value = value;
}
isLeft(): this is Left<L, A> {
return false;
}
isRight(): this is Right<L, A> {
return true;
}
}
export const left = <L, A>(l: L): Either<L, A> => {
return new Left(l);
};
export const right = <L, A>(a: A): Either<L, A> => {
return new Right<L, A>(a);
};
const negativePatientCountError = () => ({
message: 'All patient counts should be strictly positive',
});
const sumPatientCounts = (
...patientCounts: Array<number>
): Either<{ message: string }, number> => {
if (patientCounts.some(patientCount => patientCount < 0)) {
return left(negativePatientCountError());
}
return right(Math.sum(...patientCounts));
};
const globalPatientCountResult = sumPatientCounts(1, 2);
if (globalPatientCountResult.isLeft()) {
const { message } = globalPatientCountResult.value;
// do something in that case
}
const globalPatientCount = globalPatientCountResult.value;
return computeSomething(globalPatientCount);
export type Either<L, A> = Left<L, A> | Right<L, A>;
export class Left<L, A> {
// ......
applyOnRight<B>(_: (a: A) => B): Either<L, B> {
return this as any;
}
}
export class Right<L, A> {
// ......
applyOnRight<B>(func: (a: A) => B): Either<L, B> {
return new Right(func(this.value));
}
}
// ......
如果对象属于Left实例,则不执行任何操作,并将自身作为Either对象返回。
如果对象为Right实例,则按其value应用函数计算,而后返回Either对象。
const globalPatientCountResult = sumPatientCounts(1, 2);
return globalPatientCountResult.applyOnRight(globalPatientCount => {
return computeSomething(globalPatientCount);
});
// 或者更短
const globalPatientCountResult = sumPatientCounts(1, 2);
return globalPatientCountResult.applyOnRight(computeSomething);
预期内错误:有时候,大家可以很清楚错误状态的逻辑来源,而调用方能够明确理解并处理这些错误状态。以我们之前的sumPatientCounts为例,患者计数为负值显然是种不合逻辑的情况,因此属于预期内错误。每当使用患者计数负值调用函数时,都会收到错误消息,并借此意识到该领域中强制执行的业务规则。预期内错误属于DDD当中最重要的错误类型,因为其具备明确的意义并体现出基本业务逻辑。
预期外错误:另一方面,某些错误完全在预期之外,而且跟业务逻辑毫无关联。这类问题在涉及不确定性函数(例如从数据库中获取对象、存储文件或者调用外部API)时,就会表现得特别普遍。我尝试从数据库处获取对象,但对方由于网络问题而无法及时响应——这类问题纯属意外,没有丝毫的确定性,而且并不属于核心领域的组成部分(当然,除非您的核心领域就是处理数据库)。
export interface Failure<FailureType extends string> {
type: FailureType;
reason: string;
}
// 文件 : user/user.ts
// 引入....
interface UserConstructorArgs {
email: string;
firstName: string;
lastName: string;
}
export class User {
readonly email: string;
readonly firstName: string;
readonly lastName: string;
private constructor(props: UserConstructorArgs) {
this.email = props.email;
this.firstName = props.firstName;
this.lastName = props.lastName;
}
static build({
email,
firstName,
lastName,
}: {
email: string;
firstName: string;
lastName: string;
}): Either<Failure<UserError.InvalidCreationArguments>, User> {
if ([email, firstName, lastName].some(field => field.length === 0)) {
return left(invalidCreationArgumentsError());
}
return right(new User({ email, firstName, lastName }));
}
}
// 文件 : user/error.ts
export enum UserError {
InvalidCreationArguments,
}
export const invalidCreationArgumentsError = (): Failure<
UserError.InvalidCreationArguments
> => ({
type: UserError.InvalidCreationArguments,
});
聚合存储:在对聚合进行存储时,大家需要保证该聚合中的各Entities不致因故障受到影响。如果做不到这一点,业务的整体一致性将无从谈起。
领域服务:假设某项领域服务负责在两个账户之间转移资金。这时我们需要保证只在第二账户成功贷记且第一账户成功借记的前提下,才真正执行交易;如果一者出错,则取消操作。
export class TransferFundService {
// 构造函数和参数
public async transferFund(
debtor: Customer,
creditor: Customer,
dollars: number,
) {
try{
debtor.takeMoney(dollars);
// 收账者,收钱
creditor.giveMoney(dollars);
// 转账者,转钱
} catch{
// 这个实现不太好,因为我们不知道是哪项操作失败了,当然没办法处理
// 即使某项操作(比如发送邮件)不成功,也应正常执行其他的操作//
return;
}
await this.customerRepository.store(debtor);
await this.customerRepository.store(creditor);
debtor.email,
'你的转账成功 !',
);
}
}
让我们轻松检查两项操作是否成功,以及出错时究竟是哪项操作未能完成。
保证两项操作结果的语义始终不变,根据错误原因做出修正决策——避免出现转账者资金未出、收账者资金未入,却显示交易成功的情况。