领域驱动设计:异常处理
一、异常的处理
异常处理是领域模型要考虑的一部分,原因在于模型的责任不可能无限大。在遇到自己处理能力之外的情况时,要采用异常机制报告错误,并将处理权转交。异常就是这样一种机制,某种程度上,它可以保证领域模型的纯洁性,让其只关注于核心逻辑,而不用包含一堆意外情况处理代码。
(一)领域模型中不要使用错误码
除异常外,也可以使用错误码报告意外情况,但我们并不推荐这种形式。使用异常要更加灵活方便,因为如果使用错误代码,你不得不在每一个出错的地方增添一个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){...}
相关文章:
领域驱动设计:异常处理
一、异常的处理 异常处理是领域模型要考虑的一部分,原因在于模型的责任不可能无限大。在遇到自己处理能力之外的情况时,要采用异常机制报告错误,并将处理权转交。异常就是这样一种机制,某种程度上,它可以保证领域模型…...
网络网络层之(6)ICMPv6协议
网络网络层之(6)ICMPv6协议 Author: Once Day Date: 2024年6月2日 一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦… 漫漫长路,有人对你微笑过嘛… 全系列文章可参考专栏: 通信网络技术_Once-Day的博客-CS…...
《大道平渊》· 拾壹 —— 商业一定是个故事:讲好故事,员工奋发,顾客买单。
《大道平渊》 拾壹 "大家都在喝,你喝不喝?" 商业一定是个故事,人民群众需要故事。 比如可口可乐的各种故事。 可口可乐公司也只是被营销大师们, 作为一种故事载体,发挥他们的本领。 营销大师们开发故事…...
JavaScript 如何访问本地文件夹
在浏览器环境中的JavaScript(通常指的是前端JavaScript)由于安全限制,无法直接访问用户的本地文件或文件夹。这是为了防止恶意脚本访问并窃取用户的敏感数据。 但是,有几种方法可以间接地让用户选择并访问本地文件: 使…...
ArrayList顺序表简单实现
一、创建MyArrayList框架 1.1 MyArrayList 类中实现 arr 数组 import java.util.Arrays;public class MyArrayList {private int[] arr;private int usesize;private static final int P 10;public MyArrayList() {arr new int[P];} 在 MyArrayList 类内创建 arr 数组&…...
144、二叉树的前序递归遍历
题解: 递归书写三要素: 1)确定递归函数的参数和返回值。要确定每次递归所要用到的参数以及需要返回的值 2)确定终止条件。操作系统也是用栈的方式实现递归,那么如果不写终止条件或者终止条件写的不对,都…...
youtube 1080 分辨率 下载方式
YouTube 1080p Video Downloader 这张图像代表了Autodesk Maya中一个名为rocket_body_MAT的材质的着色器网络。下面是对节点及其连接的细分: 节点 place2dTexture12: 该节点用于控制2D纹理在表面上的位置映射。输出: Out UVrocket_body2.jpg: 该节点代表一个纹理文件,具体是…...
计算机网络ppt和课后题总结(下)
常用端口总结 计算机网络中,端口是TCP/IP协议的一部分,用于标识运行在同一台计算机上的不同服务。端口号是一个16位的数字,范围从0到65535。通常,0到1023的端口被称为“熟知端口”或“系统端口”,它们被保留给一些标准…...
测试基础12:测试用例设计方法-边界值分析
课程大纲 1、定义 经验发现,较多的错误往往发生在输入或输出范围的边界上,因为边界值是代码判断语句的点,一般容易出问题(数值写错、多加或丢失等号、写错不等号方向…)。所以增加对取值范围的边界数据的测试ÿ…...
AI大模型在健康睡眠监测中的深度融合与实践案例
文章目录 1. 应用方案2. 技术实现2.1 数据采集与预处理2.2 构建与训练模型2.3 个性化建议生成 3. 优化策略4. 应用示例:多模态数据融合与实时监测4.1 数据采集4.2 实时监测与反馈 5. 深入分析模型选择和优化5.1 LSTM模型的优势和优化策略5.2 CNN模型的优势和优化策略…...
【西瓜书】9.聚类
聚类任务是无监督学习的一种用于分类等其他任务的前驱过程,作为数据清洗,基于聚类结果训练分类模型 1.聚类性能度量(有效性指标) 分类任务的性能度量有错误率、精度、准确率P、召回率R、F1度量(P-R的调和平均)、TPR、FPR、AUC回归…...
使用jemalloc实现信号驱动的程序堆栈信息打印
使用jemalloc实现信号驱动的程序堆栈信息打印 本文介绍应用如何集成jemalloc,在接收到SIGUSR1信号10时打印程序的堆栈信息。 1. 编译jemalloc 首先,确保你已经编译并安装了启用prof功能的jemalloc。以下是ubuntu18.04上的编译步骤: git c…...
树的4种遍历
目录 树的四种遍历方式的总结 1. 前序遍历(Pre-order Traversal) 2. 中序遍历(In-order Traversal) 3. 后序遍历(Post-order Traversal) 4. 层序遍历(Level-order Traversal 或 广度优先遍…...
深入探讨5种单例模式
文章目录 一、对比总览详细解释 二、代码1. 饿汉式2. 饱汉式3. 饱汉式-双检锁4. 静态内部类5. 枚举单例 三、性能对比 一、对比总览 以下是不同单例模式实现方式的特性对比表格。表格从线程安全性、延迟加载、实现复杂度、反序列化安全性、防反射攻击性等多个方面进行考量。 …...
SPOOL
-----How to Pass UNIX Variable to SPOOL Command (Doc ID 1029440.6) setenv只有csh才有不行啊PROBLEM DESCRIPTION: You would like to put a file name in Unix and have SQL*Plus read that file name, instead of hardcoding it, because it will change.You want to pa…...
挑战绝对不可能:再证有长度不同的射线
黄小宁 一空间坐标系中有公共汽车A,A中各座位到司机处的距离h是随着座位的不同而不同的变数,例如5号座位到司机处的距离是h3,…h5,…。A移动了一段距离变为汽车B≌A,B中5号座位到司机处的距离h’h3,…h’h5…...
【机器学习】Python与深度学习的完美结合——深度学习在医学影像诊断中的惊人表现
🔥 个人主页:空白诗 文章目录 一、引言二、深度学习在医学影像诊断中的突破1. 技术原理2. 实际应用3. 性能表现 三、深度学习在医学影像诊断中的惊人表现1. 提高疾病诊断准确率2. 辅助制定治疗方案 四、深度学习对医疗行业的影响和推动作用 一、引言 随着…...
MapStruct的用法总结及示例
MapStruct是一个代码生成器,它基于约定优于配置的原则,使用Java注解来简化从源对象到目标对象的映射过程。它主要用于减少样板代码,提高开发效率,并且通过编译时代码生成来保证性能。 我的个人实践方面是在2021年前那时候在项目中…...
redis 05 复制 ,哨兵
01.redis的复制功能,使用命令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一下...
强大的.NET的word模版引擎NVeloDocx
在Javer的世界里,存在了一些看起来还不错的模版引擎,比如poi-tl看起来就很不错,但是那是人家Javer们专属的,与我们.Neter关系不大。.NET的世界里Word模版引擎完全是一个空白。 很多人不得不采用使用Word XML结合其他的模版引擎来…...
基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销,平衡网络负载,延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...
基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...
GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...
