一、异常的处理
异常处理是领域模型要考虑的一部分,原因在于模型的责任不可能无限大。在遇到自己处理能力之外的情况时,要采用异常机制报告错误,并将处理权转交。异常就是这样一种机制,某种程度上,它可以保证领域模型的纯洁性,让其只关注于核心逻辑,而不用包含一堆意外情况处理代码。
(一)领域模型中不要使用错误码
除异常外,也可以使用错误码报告意外情况,但我们并不推荐这种形式。使用异常要更加灵活方便,因为如果使用错误代码,你不得不在每一个出错的地方增添一个if语句。不管是对于领域模型还是它的调用者来说,这都是个坏消息。而异常可以使我们的代码更简洁,遇到问题抛出即可。同时,它可以包含丰富的领域信息和业务逻辑,而不仅仅是语言层面的错误。
另外,异常是经过精心定义的方法失败模型,因此各种工具(如监控)可能会随时注意到异常的发生。比如,性能监视器会对异常进行追踪统计,而错误代码这种形式就没有这些优点。
如果事件是模型的一种特殊逻辑扩展机制,那么异常就是一种特殊的意外情况处理机制。虽然它们不像其他模型成员那么直观易懂,但它们对于保持模型的纯洁性和扩展性有着不可替代的作用,因此我们在建模时应予以考虑。
(二)自定义异常
使用好异常的关键在于让它表达一定的领域含义,即细分模型不愿处理的条件,抛出有领域含义的异常,以便让合适的上级调用者找到合适的处理方式。显然,“购物车已满”的异常比“数组越界”的异常更容易让调用者知道如何处理。
有些专家建议,当语言框架中已有相应异常时,不要自己创建异常,这适用于语言级别的异常。对于领域层来说,自定义异常是领域逻辑的一部分,它可以丰富通用语言。相比于错误代码,自定义异常能够很自然地被领域专家所理解。
public class FullCartException extends DomainException {private String error;private int maxCount;public FullCartException(String msg){}public FullCartException(String msg){this.error = msg;}public String getError(){return this.error;}
}
自定义异常可以继承任何语言中已有的异常,本例继承自领域异常DomainException基类。
以下是自定义异常的注意点:
- 要避免太深的继承层次,一般Exception类即可满足要求。
- 一定要以Exception作为后缀。
- 要使异常可序列化。为了使异常能够跨应用程序和跨远程边界工作,这样做是必须的。
- 要把与安全性有关的信息保存在私有的异常中,确保只有可信赖的代码才能得到该信息。比如数据库连接抛出的各类异常,可能会泄露你的表命名、表结构等信息。
- 可以为异常定义属性,这样就能从程序中取得与异常有关的额外信息。
(三) 抛出异常
设计了自定义异常以后,接下来就要决定何时抛出它了。何时抛出异常呢?
当领域模型不能或不愿处理某些意外情况时,此时应该抛出异常,将其处理权交给上一级调用者,当然,如果调用者不愿处理,则可以继续向上抛出,最后异常可能被抛到应用层,这也是很常见的情况。
这里使用了“意外情况”而不是“错误”,因为前者更符合实际含义。“不能处理”的原因是,领域逻辑并不知道合适的处理方法,可能交由他人更合适。“不愿处理”则是考虑到领域模型的纯粹性,不适宜放入与领域逻辑不相关的代码。
另外要注意,既然领域模型不处理并将处理权转交了,那么程序也就无法继续了。任何方法在抛出异常后,后面的代码都不会被执行(除了finally中的代码)。这很好理解,因为后面的代码是为正常情况准备的,而现在面对的是异常状况,自然后面的逻辑也用不到了。
在上面例子的添加购物车商品方法中,如果数量超出了购物车的最大容量,可以使用throw new FullCartException(“购物车已满”)抛出异常。抛出异常的注意点如下:
- 在领域模型中,要使用异常来处理意外情况而不是错误码。
- 不要在能处理的正常流程中抛出异常。
- 要为所有的自定义异常构建一份文档,使开发人员能够掌握,让他们能使用最合理、最具针对性的异常,比如不要使用“集合超容”来描述“购物车已满”。
- 在异常消息中避免使用感叹号和问号。
- 注意异常消息的本地化。
(四)处理异常
处理异常使用的是try…catch…finally代码结构,在catch块中处理try块可能抛出的异常,另外,finally中的代码在遇到异常后也会被执行,这也是一种保护机制,一般要在其中释放一些占用的资源。
要注意,如果你不想处理该异常,大可不必捕获,可允许异常沿着调用栈向上传递。捕获特定异常的语法是catch(fileNotFoundException e),不要省略括号这部分,也不要捕获Exception基类,因为这会捕获所有异常,通常是没必要的,而且可能吞掉有用的异常信息,而让软件行为或交互变得奇怪。
比如,商品添加不到购物车内,用户却得不到任何提醒。定义合适的富有领域逻辑的异常,并在模型遇到意外情况时及时抛出,是完成领域模型设计并保证其纯洁性的重要工作。
定义合适的富有领域逻辑的异常,并在模型遇到意外情况时及时抛出,是完成领域模型设计并保证其纯洁性的重要工作。
二、异常的分层
不管是遵循分层架构,还是菱形对称架构,都可以针对异常划分层次,并通过为异常建立统一的层超类,来统一对异常的处理。
领域层的异常层超类为DomainException,应用层的异常层超类为ApplicationException,网关层不需要考虑自定义异常,因为它的实现代码抛出的异常属于访问外部资源的基础设施框架。
领域层通过自定义异常表现领域校验逻辑与错误消息,到了应用层,又保证了异常的统一性。
在编写领域层的代码时,对异常的态度为“只抛出,不捕获”,将所有领域层的异常带来的错误和隐患,都交给外层的应用服务。应用服务对待异常的态度迥然不同,采用了“捕获底层异常,抛出应用异常”的设计原则。
(一)应用异常
为了让应用服务告知远程服务调用者究竟是什么样的错误导致异常抛出,可以分别为应用层定义如下3种异常子类,均派生自ApplicationException类型:
- ApplicationDomainException,由领域逻辑错误导致的异常;
- ApplicationValidationException,由输入参数验证错误导致的异常;
- ApplicationInfrastructureException,由基础设施访问错误导致的异常。
遵循了分层的异常设计原则后,可以考虑将异常的层超类定义为非受控异常RuntimeException的子类,如此就可以避免异常对接口方法的污染。
建议将应用接口、应用模型、应用异常等均放在API包中,因为应用模型和异常也是API的一部分;
(二)自我验证
如果验证逻辑相对复杂,就建议将验证逻辑的细节提取到一个私有方法validate(),确保构造函数的实现更加简洁。
例如,一个代表邮政编号的ZipCode值对象:
public class ZipCode {private final String zipCode;private ZipCode(String zipcode){validate(zipcode);this.zipCode = zipcode;}private void valideate(String zipCode){if(isEmptyOrNull(zipcode)) throw new InvalidZipCodeException("邮政编码不能为空");if(!isValid(zipcode)) throw new InvalidZipCodeException("邮政编码需要是有效的");}
}
自我验证方法保证了值对象的正确性。如果我们将每个组成实体属性的值对象都定义为具有自我验证能力的类,就可以使得组成程序的基本单元变得更加健壮,间接提高了整个软件系统的健壮性。值对象的验证逻辑是领域逻辑的一部分,我们应为其编写单元测试。
自我验证的领域行为仅验证外部传入的设置值。倘若验证功能还需求助外部资源,例如查询数据库以检查name是否已经存在,这样的验证逻辑就不再是“自给自足”的,不能交由值对象承担。
三、异常处理的错误模式
(一)异常淹没
程序捕获某种异常,但未对异常 进行正确处理,导致异常信息淹没。
1.忽略异常
忽略异常,对异常不作任何处理,忽略异常处理会使程序泄露意想不到的状态信息。
try(FileInputStream is = new FileInputStream(name)){}
catch(FileNotFoundException e){}
2.异常消失
异常消失,Java支持try/finally语法。若finally模块包含return语句,则会抑制异常的抛出,使异常丢失。
try{throw new MagicException();
}finally{if(retrunFromFinally())return;
}
3.不使用具体的异常
不使用具体的异常,在方法中不抛出适当的异常,而是普通的Exception或者Throwable会导致异常淹没。因多数异常都直接或间接从java.lang.Exception派生,catch(Exception e)处理几乎所有异常。普通的异常使调用者无法确定发生异常的具体种类。
try{
...
}
catch(Exception e){
e.printStackTrace();
}
(二)异常使用不当
程序捕获所有异常是个好想法,但太广泛地捕获异常或对其使用不正确则会影响程序执行效率甚至威胁程序的安全。
1.使用程序捕获 NullPointerException、0utMemoryError等非检查异常。
try{
method();
}
catch(NullPointerException npe){}
2.unlock位置不当
在try内部发生异常,以致unlock()无法调用,上锁后不释放引起死锁。正确处理是在finally中显式释放。
public class MyClass {private final ReentrantLock lock = new ReentrantLock();private int sharedResource = 0;public void incrementSharedResource() {lock.lock(); // 上锁try {// 模拟一些可能抛出异常的操作if (sharedResource < 0) {throw new IllegalStateException("Shared resource cannot be negative");}sharedResource++;// ... 其他操作 ...} catch (IllegalStateException e) {// 异常处理,但没有释放锁System.err.println("Exception occurred: " + e.getMessage());// 注意:这里我们忘记了释放锁}// 如果异常在try块中发生,lock.unlock()将不会被调用,导致死锁}
}
3.控制流中使用异常
异常只用在异常条件下 ,不能用于正常的控制流 。将异常用于控制流会降低代码可维护性和可读性 。
try{Iterator i = collection.iterator();while(true){Foo foo = i.next();...}
}
catch(NoSuchElementException e){...}