领域驱动设计:异常处理

一、异常的处理

异常处理是领域模型要考虑的一部分,原因在于模型的责任不可能无限大。在遇到自己处理能力之外的情况时,要采用异常机制报告错误,并将处理权转交。异常就是这样一种机制,某种程度上,它可以保证领域模型的纯洁性,让其只关注于核心逻辑,而不用包含一堆意外情况处理代码。

(一)领域模型中不要使用错误码

除异常外,也可以使用错误码报告意外情况,但我们并不推荐这种形式。使用异常要更加灵活方便,因为如果使用错误代码,你不得不在每一个出错的地方增添一个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){...}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/25704.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

06-服务拆分-服务远程调用

06-服务拆分-服务远程调用 1.根据订单id查询订单功能 需求:根据订单id查询订单的同时,把订单所属的用户信息一起返回 2.远程调用方式分析: 1.注册RestTemplate ​ 在order-service的OrderApplication中注册RestTemplate 代码: @MapperScan("cn.itcast.order.ma…

Python 设计模式(结构型)

文章目录 代理模式场景示例 门面模式场景示例 桥接模式场景示例 适配器模式场景示例 外观模式对比门面模式场景示例 享元模式场景示例 装饰器模式场景示例 组合模式场景示例 代理模式 在Python中&#xff0c;代理模式是一种结构型设计模式&#xff0c;它允许你提供一个代理对象…

grok debugger 正则解析 网络安全设备日志

1、网络设备、安全设备不同品牌、不同型号的设备&#xff0c;日志格式都不一样&#xff0c;那针对这种情况&#xff0c;我们可以使用工具grok debugger进行日志格式解析&#xff0c;具体的网址为&#xff1a; 地址:https://grokdebug.herokuapp.com/ 也可以采用私有化部署&am…

使用Python去除PNG图片背景

要使用Python自动去除PNG图片的背景&#xff0c;你可以使用remove.bg的API&#xff0c;或者使用一些图像处理库如OpenCV和Pillow结合Mask R-CNN等深度学习模型。以下是一个使用Pillow库的简单示例&#xff1a; 安装所需库&#xff1a; pip install pillow numpy使用以下代码去…

归并排序的递归与非递归实现

递归实现 归并排序有点类似于二叉树的后序遍历&#xff0c;是一种基于分治思想的排序算法。具体过程如下&#xff1a; 但要注意&#xff0c;在归并时要额外开辟一个与原数组同等大小的空间用来存储每次归并排序后的值&#xff0c;然后再拷贝到原数组中。 代码实现&#xff1a…

【十大排序算法】归并排序

归并排序&#xff0c;如同秋日落叶&#xff0c;分散而细碎&#xff0c; 然而风吹叶动&#xff0c;自然而有序&#xff0c; 彼此相遇&#xff0c;轻轻合拢&#xff0c; 最终成就&#xff0c;秩序之谧。 文章目录 一、归并排序二、发展历史三、处理流程四、算法实现五、算法特性…

树莓派4B_OpenCv学习笔记5:读取窗口鼠标状态坐标_TrackBar滑动条控件的使用

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1&#xff1a; 今日学习:读取窗口鼠标状态坐标_TrackBar滑动条控件的使…

自然资源-《乡村振兴用地政策指南(2023年)》解读

自然资源-《乡村振兴用地政策指南&#xff08;2023年&#xff09;》解读 近期&#xff0c;自然资源部办公厅印发《乡村振兴用地政策指南&#xff08;2023年&#xff09;》&#xff08;以下简称《指南》&#xff09;。作为第一部针对乡村振兴用地政策的“工具包”&#xff0c;《…

Vue.js基础入门

Vue.js的基本概念和框架结构 Vue.js的基本概念 Vue实例 Vue实例是通过new Vue()创建的&#xff0c;它是Vue应用的核心。每个Vue应用都是由一个Vue实例开始的。示例代码&#xff1a; var app new Vue({el: #app,data: {message: Hello Vue!} });数据绑定 Vue.js提供了双向数据…

redis 05 复制 ,哨兵

01.redis的复制功能&#xff0c;使用命令slaveof 2. 2.1 2.2 3. 3.1 3.1.1 3.1.2 3.1.3 4 4.1 4.2 例子 5.1 这里是从客户端发出的指令 5.2 套接字就是socket 这里是和redis事件相关的知识 5.3 ping一下

idea编码问题:需要 <标识符> 非法的类型 、需要为 class、interface 或 enum 问题解决

目录 问题现象 问题解决 问题现象 今天在idea 使用中遇到的一个编码的问题就是&#xff0c;出现了这个&#xff1a; Error:(357, 28) java: /home/luya...........anageService.java:357: 需要 <标识符> Error:(357, 41) java: /home/luya............anageService.ja…

Cinema 4D 2024 软件安装教程、附安装包下载

Cinema 4D 2024 Cinema 4D&#xff08;C4D&#xff09;是一款由Maxon开发的三维建模、动画和渲染软件&#xff0c;广泛用于电影制作、广告、游戏开发、视觉效果等领域。Cinema 4D允许用户创建复杂的三维模型&#xff0c;包括角色、场景、物体等。它提供了多种建模工具&#x…

Channels无法使用ASGI问题

Django Channels是一个基于Django的扩展, 用于处理WebSockets, 长轮询和触发器事件等实时应用程序. 它允许Django处理异步请求, 并提供了与其他WebSockets库集成的功能.当我们在Django Channels中使用ASGI_APPLICATION设置时, 我们可以指定一个新的ASGI应用程序来处理ASGI请求.…

Day01 - Day05

Day01 - Day05 Day01&#xff08;1997年Text1&#xff09; After six months of arguing and final 16 hours of hot parliamentary debates, Australia’s Northern Territory became the first legal authority in the world to allow doctors to take the lives of incurab…

java非框架代码实现缓存并实现自动过期

要实现一个简单的Java缓存&#xff0c;可以使用ConcurrentHashMap和ScheduledExecutorService。以下是一个简单的示例&#xff1a; java import java.util.concurrent.*; public class SimpleCache<K, V> { private final ConcurrentHashMap<K, CacheItem<V>&g…

springboot中基于RestTemplate 类 实现调用第三方API接口,获取响应体内容不需要转换数据类型【丰富版】

RestTemplate 用法 和 http工具类 这篇就不说了 可以去看下面的博客 本篇文章是 针对 下面的博客 进行的扩展 https://blog.csdn.net/Drug_/article/details/137166797 我们在调用第三方 api接口 时候 在获取相应体的时候 不知道用什么数据类型 去接 响应体里的数据 用 字符串…

数据库期末设计——图书管理系统

目录 1.前置软件以及开发环境&#xff1a; 2.开发过程讲解 代码环节&#xff1a; 数据库代码 1.BookDao.java 2.BookTypeDao.java 3.UserDao.java 4.Book.java 5.BookType.java 6.User.java 7.DbUtil.java 8.Stringutil.java 9.BookAddInterFrm.java 10.BookMan…

前端学习----css基础语法

CSS概述 CAscading Style Sheets(级联样式表) CSS是一种样式语言,用于对HTML文档控制外观,自定义布局等,例如字体,颜色,边距等 可将页面的内容与表现形式分离,页面内容存放在HTML文档中,而用于定义表现形式的CSS在一个.css文件中或HTML文档的某一部分 HTML与CSS的关系 HTM…

freertos中的链表1 - 链表的数据结构

1.概述 freertos中链表的实现在 list.c 和 list.h。旨在通过学习freertos中的链表的数据结构&#xff0c;对freertos中的链表实现有一个整体的认识。freertos使用了三个数据结构来描述链表&#xff0c;分别是&#xff1a;List_t&#xff0c; MiniListItem_t&#xff0c;ListIt…

智能合约中时间依赖漏洞

时间依赖漏洞 时间依赖漏洞是智能合约中一个常见的安全问题&#xff0c;特别是在以太坊等区块链环境中。这是因为区块链的区块时间戳可以被矿工在一定程度上操纵&#xff0c;这使得依赖于时间戳的智能合约容易受到攻击。攻击者可以通过控制区块时间戳来触发合约中的某些条件&a…