你是否曾经发现自己想知道到底应该在哪里抛出一个错误,以便由try-catch来消耗?你是否有时会出现多层的try-catch块?你是否应该直接返回null来代替?
如果你的回答是否定的,我会很惊讶。作为一个曾经的新开发者,从初级Java开发者到Node.js开发者,再到TypeScript的狂热者,我已经经历了数百次。
考虑一下创建一个User 对象。我们需要传入几个需要验证的参数,以便创建一个。
class User {
public email: string;
public firstName: string;
public lastName: string;
private constructor (email: string, firstName: string, lastName: string): User {
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
public static createUser (email: string, firstName: string, lastName: string): User {
if (!isValidEmail(email)) {
throw new Error("Email is invalid")
}
// .. validate firstName
// .. validate lastName
// return new user
}
}
这是一个相当常见的情况。在这一点上,我们可能会考虑,"从这个类中捕捉这个错误有意义吗?"。
不,那是没有意义的。在User 类中捕捉创建错误有什么好处?它的全部目的是被其他东西所使用。即使如此,我们会怎么做呢?返回null ?这不是一个好主意。调用代码希望从这个方法中得到一个User 。
好吧,这意味着任何创建User的类的方法都需要确保他们用一个try-catch 块来包装创建一个User 。
我不认为这是一个好的方法来处理如此微不足道的操作。
为什么有目的的抛出错误不一定是最好的选择?
请注意我说*"不一定是最好的选择 "*是多么谨慎。这是因为有时它是一个好的选择。
但是如果像创建新对象这样的事情如此危险,它就会对调用的代码进行约束。这是一种代码的味道,如果我曾经闻到过的话。
我们不想这样做的另一个原因是,使用throw 关键字并不是非常类型安全的。
当我们使用throw 关键字时,我们打破了代码的流程,跳到了最近的错误处理程序(如果它存在的话,而且最好存在,否则我们会得到一个uncaughtException错误)。
支持和反对这种行为的观点都有,但对于我们的需求来说,我们想要的是可预测的、类型安全的程序行为。这也是我们这么多人首先被吸引到TypeScript的主要原因之一。
是的,你可以记住把try-catch 块放在所有的地方,并预测将要发生的事情,但同样,编译器在这方面完全没有帮助你。这都是你自己的事。
介绍一下结果类
我第一次发现结果类是在Vladimir Khorikov的pluralsight课程中学习[贫血领域模型]的时候。
这是他的C#结果类,转换为TypeScript。
export class Result<T> {
public isSuccess: boolean;
public isFailure: boolean
public error: string;
private _value: T;
private constructor (isSuccess: boolean, error?: string, value?: T) {
if (isSuccess && error) {
throw new Error(`InvalidOperation: A result cannot be
successful and contain an error`);
}
if (!isSuccess && !error) {
throw new Error(`InvalidOperation: A failing result
needs to contain an error message`);
}
this.isSuccess = isSuccess;
this.isFailure = !isSuccess;
this.error = error;
this._value = value;
Object.freeze(this);
}
public getValue () : T {
if (!this.isSuccess) {
throw new Error(`Cant retrieve the value from a failed result.`)
}
return this._value;
}
public static ok<U> (value?: U) : Result<U> {
return new Result<U>(true, null, value);
}
public static fail<U> (error: string): Result<U> {
return new Result<U>(false, error);
}
public static combine (results: Result<any>[]) : Result<any> {
for (let result of results) {
if (result.isFailure) return result;
}
return Result.ok<any>();
}
}
使用这个类有很多好处。它使我们能够。
- 安全地返回错误状态
- 返回有效的结果
- 结合几个结果并确定整体的成功或失败状态
有了一个新的Result<T> 实例,我们可以。
- 用
isSuccess,检查是否有效 - 用检查失败
isFailure - 收集错误
error - 收集值
getValue() - 使用
Result的数组来检查其有效性。Result.combine(results: Result[])
使用结果类
让我们调整一下User 类,从静态createUser() 工厂方法返回一个Result<User> ,而不是明确地抛出一个错误。
class User {
public email: string;
public firstName: string;
public lastName: string;
private constructor (email: string, firstName: string, lastName: string): User {
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
public static createUser (email: string, firstName: string, lastName: string): Result<User> {
if (!isValidEmail(email)) {
return Result.fail<User>('Email is invalid')
}
if (!!firstName === false && firstName.length > 1 && firstName.length < 50) {
return Result.fail<User>('First name is invalid')
}
if (!!lastName === false && lastName.length > 1 && lastName.length < 50) {
return Result.fail<User>('Last name is invalid')
}
return Result.ok<User>(new User(email, firstName, lastName));
}
}
注意:另一个潜在的重构是将验证规则放在email,firstName 和lastName 的Value Objects中。
然后让我们从一个父类创建一个User 。
class CreateUserController {
public executeImpl (): void {
const { req } = this;
const { email, firstName, lastName } = req.body;
const userOrError: Result<User> = User.create(email, firstName, lastName);
if (userOrError.isFailure) {
return this.fail(userOrError.error)
}
const user: User = userOrError.getValue();
// persist to database ...
}
}
瞧!这就对了。
如果我们使用Value Objects,我们可以使用Result.combine() 方法来一次验证一个数组的Result,就像这样。
class CreateUserController {
public executeImpl (): void {
const { req } = this;
const { email, firstName, lastName } = req.body;
const emailOrError: Result<Email> = Email.create(email);
const firstNameOrError: Result<FirstName> = FirstName.create(firstName);
const lastNameOrError: Result<LastName> = LastName.create(lastName);
const userPropsResult: Result<any> = Result.combine([
emailOrError, firstNameOrError, lastNameOrError
])
// If this failed, it will return the first error that occurred.
if (userPropsResult.isFailure) {
return this.fail(userPropsResult.error)
}
const userOrError: Result<User> = User.create(
emailOrError.getValue(),
firstNameOrError.getValue(),
lastNameOrError.getValue()
);
if (userOrError.isFailure) {
return this.fail(userOrError.error)
}
const user: User = userOrError.getValue();
// persist to database ...
}
}
这就是了!这就是我们如何使用Result 类来让编译器帮助我们处理预期的边缘案例错误。
在有些情况下,有目的地抛出错误确实有很大的意义!
什么时候要有目的地抛出错误
答:当你正在开发一个库或一个工具,供其他开发者使用。
在这种情况下,你不想强迫他们使用我们喜欢的Result 类或任何其他非标准的方法来捕捉错误。我们应该让他们自己来决定。
虽然,在JavaScript世界中,一个常见的惯例是将错误作为回调的第一个参数返回。
例子:将回调错误包装成被拒绝的承诺
实现Redisnpm包的开发者决定,他们想用回调的方式来报告错误。
client.get(key,
(error: Error, reply: unknown) => {
if (error) {
// handle error
} else {
// handle reply
}
});
在我消费这个库的实际应用程序代码中,我通常会将这些包裹在一个Promises中,以便被我的其他代码用async/await使用。
import { RedisClient } from 'redis'
export abstract class AbstractRedisClient {
protected client: RedisClient;
constructor (client: RedisClient) {
this.client = client;
}
public getOne<T> (key: string): Promise<T> {
return new Promise((resolve, reject) => {
this.client.get(key,
(error: Error, reply: unknown) => {
if (error) {
return reject(error)
} else {
return resolve(<T>reply);
}
});
})
}
}
当你处理API、外部资源或其他对外界的适配器时,需要创建你自己的适配器来使用它是一件很常见的事情,这样你就可以在自己的代码库中使用自己的代码风格安全地引用它。
但是,如果需要为每个类(如User 类)创建一个Adapter,以便安全地封装它们,那就不太干净了......所以我们需要别的东西。
B: 当我们遇到我们不期望或不知道如何处理的错误时。
有点像对A 的延伸,因为当我们在处理库的代码时,我们并不真正知道将来使用我们代码的人打算如何处理错误,我们只需要让他们知道错误正在发生。
对于那些我们不知道如何处理的错误,通常,我们希望取消正在进行的操作,因为发生了一些不好的事情。
这些错误真的会把我们想做的事情弄得一团糟,而且可能是由无限可能的事情引起的,我们没有想到或假设会发生。
- 数据库连接问题
- 代码错误
- 空指针错误(有时)
- 内存不足
如果是一个http请求,我们可能会向客户端抛出一个500 错误。
如果是一个脚本,我们可能会以一个非零的错误代码退出。
或者,是的,使用throw 语句,但只有当你写的代码要被别人使用,而你又不知道他们打算如何使用它的时候。
在这种情况下,我们真正有意义的是杀死我们正在做的任何事情,直接退出或取消网络请求。
总结
有些开发者在这方面可以做得非常非常花哨。我个人并没有对这个问题做太多的研究,但它源于单体和类似的东西,如果你把它做到极致,你可以得到一些非常有趣的类似于Rxjs的结果。
我不认为这种编程方式是主流,以至于我现在还不想向我的同龄人大力宣传它,因为即使是Rx.js有时也是一种挑战。也许在某个时候,我会找到时间去学习更多。