戏说领域驱动设计(十八)——内验

Python微信订餐小程序课程视频

https://edu.csdn.net/course/detail/36074

Python实战量化交易理财系统

https://edu.csdn.net/course/detail/35475
  验证在我们现实的生活中非常常见,比如您找工作得先整个面试验证你的能力是否靠谱;找对象得先验证下对方的颜值和升值空间。有些工程师写代码从不验证,我觉得是有三个原因,一是意识不够,过于相信前端或外部服务;二是个人缺少主动思考的能力;三是团队负责人的问题,您都当了领导了为什么不制定一些基本开发规则给团队树规矩。实际上,验证这个事情说简单也的确不难,不就是个值判断吗?可如果想把这个事情做好还真是一个需要值得思考的工作,就和异常的处理一样,我告诉你就算干了10年的开发都未必知道怎么有效的使用异常。代码里中充满了土味,一看就特Low。所以我们把验证这个事情单独的提出来,越是越是简单的东西想写好才越难。

您应该不知道“对象不变性”这个名字吧?领域模型包括实体与值对象都需要遵循这个规则,就是说不论你对一个领域对像做什么操作,不论怎么盘它,其本质应该保持不变。不都说“江没易改,本性难变”吗?上述的操作不仅是调用对象上的方法,还包括构造对象的过程。有一个例子说“一个没有角的独角兽还能称得上是独角兽吗?”,简单来说就是你需要始终保持领域对象处于合法的状态或者说是属性的值不能超出业务规则限制。比如订单对象:客户信息不能为空、价格信息不能为负数等、订单项数量要大于0小于100等,不论你在订单对象上做什么操作,这些属性值都不可以超出约束。

想要保证对象的“不变性”,不能依赖于前端的输入和数据库本身的约束,那些基本都不靠谱,最好的方式还是首推“验证”。针对对象本身是否合法的验证我称之为“内验”。相对的,验证某个业务先决条件的验证称之为“外验”,因为此时的验证已经超出了对象本身的规则范围。既然需要对所有的对象都进行验证,就应该将其做为一种通用的能力放到OOP编程框架中。其实我个人特别不喜欢称之为“框架”,感觉概念太大了,所以我们就称呼为基础类库吧。这个类库可以提供一些用于检验领域对象是否合法的工具随用随取,不用重复的造轮子。

领域对象内验的实现思想很简单:为每个领域对象中加入用于验证的方法和验证规则,在对象创建后或持久化前通过调用每个对象的验证方法实现验证逻辑。您一定要注意前面这句话中所说的触发验证方法的时机,别回头不管什么场景就调用验证,这叫过度设计,那代码会让人吐的。另外,既然是通用的能力而且用于验证领域对象,就最好将其放到领域模型的基类中按需在具体类中进行重写,所以就让我们从这些基类作为起点开搞。

一、验证服务基类

看过前面的文章您应该已经知道了我们在实现验证的时候使用了一种类“规约模式”,也就是将验证规则嵌入到领域对象中,并在合适的时机进行验证方法的调用。为此,在设计领域模型基类的时候我们让其继承的了一个用于验证的父类“ValidatableBase”,这个类里面包含了两个方法,具体代码如下所示。“ValidatableBase”是一个抽象类,实现了接口“Validatable”,这个接口很重要,但凡需要验证的对象都会实现这个接口。 您可以看一下下面的类图,说得挺绕其实就三个组件。

public interface Validatable {/*** 验证* @return 验证结果*/ParameterValidationResult validate();
}
public abstract class **ValidatableBase** implements Validatable {/*** 验证当前领域模型* @return 验证的结果*/final public ParameterValidationResult **validate**() {RuleManager ruleManager = new RuleManager(this);this.addRule(ruleManager);return ruleManager.validate();}/*** 增加验证规则* @param ruleManager 验证规则管理器*/protected void **addRule**(RuleManager ruleManager) {}
}
public abstract class DomainModel extends ValidatableBase {   protected void **addRule**(RuleManager ruleManager) {}}

“ValidatableBase”类中的方法“validate”用于触发模型的验证。方法“addRule”用于将验证规则加入到一个包含了验证规则列表的对象“RuleManager”中,所以你可根据需要决定是否在具体类中进行方法的重写,比如上面的“DomainModel”中我就对它进行了覆盖。当触发验证的时候,只需要遍历这个“RuleManager”对象中的每个规约并将验证结果合并即可实现统一验证的目的,RuleManager代码可参看如下片段。

public class RuleManager implements Validatable {//规则拥有者private DomainModel owner;//规则列表private List rules = new ArrayList();/*** 增加规则* @param rule 规则对象*/public void addRule(Rule rule){if(rule != null){rules.add(rule);}}public RuleManager(DomainModel owner){this.owner = owner;}/*** 执行验证,调用规则的验证方法来执行具体的验证。* @return 验证结果*/public ParameterValidationResult validate(){CompositeParameterValidateResult result = new CompositeParameterValidateResult();for(Rule rule : this.rules){//针对嵌入式对象的验证if (rule instanceof EmbeddedObjectRule){**EmbeddedObjectRule embeddedObjectRule** **=** **(EmbeddedObjectRule) rule;**ParameterValidationResult validationResult = embeddedObjectRule.getTarget().validate();if(!validationResult.isSuccess()){result.addValidationResult(new ParameterValidationResult(false, validateHandlingResult.getMessage()));}continue;}**ParameterValidationResult ruleVerifyResult** **= rule.validate();** if(!ruleVerifyResult.isSuccess()){result.fail();result.addValidationResult(new ParameterValidationResult(false, errorMessage));}}return result;}
}

这里面其实最有意思也最值得一说的是“EmbeddedObjectRule”这一段,其用于对内嵌对象进行验证。所谓的内嵌对象是指包含于其它对象内部的领域对象,比如下面代码片段中的“contact”就是一个嵌套对象。我们验证领域对象的时候不仅要验证每个简单类型的属性,还需要验证其中嵌入的其它对象。通过这种方式,就可以实现一层层的验证,使得每个属性都能被检验到。在上面代码中另外一个有意思的地方是这段“ParameterValidationResult ruleVerifyResult = rule.validate();”,您会发现真正执行验证操作的其实是“Rule”对象,这些是我们预定好的一组规则,当然您也可以通过实现“Rule”接口自行加入新的规则。使用预定义规则的方式能加速开发的速度,让我们拎包即可入住。由“Rule”做验证其实是OOP中使用较为频繁的方式,把责任分配的非常明确,十分有利用扩展。

public class Order extends EntityModel {private String name;**private Contact contact;** protected Order(Long id, String name, Contact contact) throws OrderCreationException {super(id);this.name = name;this.contact = contact;}@Overrideprotected void addRule(RuleManager ruleManager) {super.addRule(ruleManager);**ruleManager.addRule(****new EmbeddedObjectRule("contact", this****.contact));**  }public String getName() {return name;}public Contact getContact() {return contact;}
}

验证规则定义了待验证目标需要满足什么样的规范,由于规则间有一些通用的属性,所以我们在设计的时候首先会引入一个“RuleBase”基类,所有的规则都会从他继承。“RuleBase”实现了“Rule”接口,而“Rule”也对前面我们说过的“Validatable”进行了扩展。类图与代码如下所示,其实也是三个组件。

public interface Rule extends Validatable {/*** 与操作* @param rule 目标规则* @return 与后的规则*/Rule and(Rule rule);/*** 或操作* @param rule 目标规则* @return 或后的规则*/Rule or(Rule rule);
}
public abstract class RuleBaseextends DomainModel> implements Rule {//验证的目标private TTarget target;//验证目标的名称private String nameOfTarget;//当规验证失败时的错误提示信息private String customErrorMessage = GlobalConstants.EMPTY\_STRING;/*** 规则基类* @param nameOfTarget 验证目标的名称* @param target 验证的目标*/protected RuleBase(String nameOfTarget, TTarget target){this(nameOfTarget, target, new String());}/*** 与操作* @param rule 目标规则* @return 与后的规则*/@Overridepublic Rule **and**(Rule rule) {return new AndRule(this, (RuleBase)rule);}/*** 或操作** @param rule 目标规则* @return 或后的规则*/@Overridepublic Rule **or**(Rule rule) {return new OrRule(this, (RuleBase)rule);}
}

“RuleBase”类里除了包含了共用属性外,还实现了两个逻辑操作“与”和“或”,也就是说您可以实现规则的组合,比如我们要求:用户名称不能为空且长度小于等于30,就可以使用下面代码表示,这样写比较优雅。

new ObjectNotNullRule("name", this.name).and(new LE("name", this.name.length(), 30))

通过上面提到的验证规则框架,我们就可以开始着手建立一些具体的规则 ,下面展示了“对象不为空”规则的代码片段,这里面需要特别关注的是方法“validate”,用于执行实际的验证逻辑。类似“大于”规则,可以通过使用“compareTo”方法实现。

public class ObjectNotNullRule extends RuleBase {/*** 获取验证失败时缺省的错误提示信息*/@Overrideprotected String getDefaultErrorMessage() {return String.format("%s为空对象", this.getNameOfTarget());}/*** 对象非空规则* @param nameOfTarget 验证目标的名称* @param target 验证的目标*/public ObjectNotNullRule(String nameOfTarget, DomainModel target) {this(nameOfTarget, target, GlobalConstants.EMPTY\_STRING);}/*** 执行验证* @return 验证是否成功*/@Overridepublic ParameterValidationResult **validate**() {if(this.getTarget() == null){return ParameterValidationResult.failed(null);}return ParameterValidationResult.success();}
}

到目前为止我们已经展示了内验所具备的一切条件,现在我们就可以在领域模型中加入各类验证规则了。下面的代码片段以上面的“ObjectNotNullRule”规则为例展示了如何在业务代码中设置验证规则。这样的代码是不是看起来非常的漂亮?至少不用写一堆的“if……else”。

public class Order extends EntityModel {private String name;private Contact contact;@Overrideprotected void addRule(RuleManager ruleManager) {super.addRule(ruleManager);ruleManager.addRule(new EmbeddedObjectRule("contact", this.contact));ruleManager.addRule(new ObjectNotNullRule("name", this.name));}
}

二、验证触发的时机

验证触发的时机是需要重点说明和解释的内容。通过上面的代码您应该可以看出来每个领域模型无论是实体还是值对象都会包含一个叫作“validate”的公有方法,既然是公有就代表您可以随意的使用,所以如果不加以限制代码就会变得特别脏……像我这种有代码洁癖的人是无论如何不能忍受的,所以我们需要确定触发验证的时机,这里给的答案很简单:对象构造完成时。对象构造包括使用构造函数和对象工厂两种方式,一旦不合法就直接抛出异常,因为不合法的对象是一个畸形儿不能该被创造出来,一般情况下也不允许创造出来后做二次加工使其合法。直白一点就是说你只能使用一行代码构造对象比如“new BusinessEntity()”或“BusinessEntityFactory.create(),比较建议使用工厂的方式创建对象以避免在构造函数中抛异常”,如果成功就返回目标对象失败则直接报错,第十七章中我展示过一个“OrderFactory”的案例,您可以翻看一下。

领域对象的创建其实也只会出现在两个时机中:新建及反序列化时。针对新建做验证是因为参数来源于用户或其它服务的输入,这些是不可信任的;而反序列化时进行验证的原因也很简单,我们在将对象序列化时它其实是合法的,不过一旦存储到比如数据库中就不可控了,您知道谁手贱把数据给改了或由于错误执行了某些脚本造成数据变质了。您不能或也不应该只依赖于数据库本身的验证规则来保障数据的正确性,使用关系型数据库还好一点,使用如MongoDB这种的,那只能看运气了。再说了,业务对象的验证属于业务代码要处理的,您把这个责任推给数据库就不合适了。

被成功创建后的对象,您就可以为所欲为的进行操作了,包括最后的持久化阶段也不需要进行二次验证(如果我在前面的文章中提及到对象在持久化时进行验证的话,请务必注意这种后验的方式很不友好。比如订单中的客户信息由于意外被置成了“null”,如果不进行构造时的检测,您在使用这个信息的时候就可能抛NPE)。这种说法应该没让您惊呆了吧?也许您可能认为这种说法非常的荒唐,我给您解释一下为什么。

首先,我们的前提是对象创建后是合法的,这个在前面已经说过,使用构造函数或工厂进行保障;第二,由于有了聚合及聚合根的概念,您不可能绕过聚合根而直接修改其聚合内部的对象。比如用户实体包含了一个值对象“实名信息”,我们在修改这个信息的时候不应该绕过用户对象而直接对其引用或修改。假如此时的用户是被冻结的状态,修改实名信息是没有意义的,违反了“客户冻结”时的业务操作限制;而通过让客户对象提供修改的方法,就可以在修改前加一些验证对操作进行限制,也就是说“只能通过聚合根修改聚合”的原则进一步保障了对象的合法性。当然了,您也可以在修改前先把客户信息查询出来判断一下状态再做变更逻辑,但这种方式会造成业务规则不够内聚,而且这也是典型的面向过程的编程思维。第三点,我假设您在调用领域对象的公有方法时已经进行了参数的验证,如果出现违反业务规则的情况则可直接抛出一个业务异常,比如“冻结的用户不能修改实名信息”这个规则,您的代码可能会按如下方式写。其实第三条的假设就不应该存在,谁写公有方法的时候不验证啊?

public class Account extends EntityModel {public void changeRealName(string name, string idCard) throws RealNameModificationException {if (**this.status ==** **AccountStatus.FREEZEN**) {throw new RealNameModificationException();}……}
}

综上三条所述,已经覆盖了您使用领域对象时涉及修改的所有场景,每一步都对对象的不变性进行了保障,那创建好的领域对象不就是您手中的小白羊吗?盘它的时候根本而不用担心它不服。

总结

对象的内验是一种验证对象合法性的手段,条条大路通罗马,在实践中其实有多种验证的方式可采用,您所关注的其实应该是它的思想。还是要多提醒一句,你应该知道在DDD中要以聚合为存储单元、事务单元,其实应该还需要多加一条:验证单元,上述所说的验证是以聚合为单位的而非某一个实体或值对象。在实践中您需要多去思考对象的合法性,虽然说不太可能一下子都想全了,但要有一个验证意识。这样的代码安全性才高。其实不论是做什么样的系统,应该对安全抱有敬畏的态度,今天多想一点,明天您就少吃点亏。

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

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

相关文章

从Zabbix数据库中提取内存采集的数据,做内存使用率计算

背景需求很简单,分析所有的设备的内存使用率,看那些设备的内存不够用是否需要加内存。。。下面的脚本逻辑,就是通过提取zabbix数据库中的hostid,在提取itemid。。然后通过item name过滤提取趋势数据,获取一天中最大的内…

Annotation版本的HelloWorld

hiberante 的 annotation历史: 在hibernate3以后,开始支持Annotation; 先有hiberante再有JPA,有了JPA标准之后,hibernate写了Annotation来支持JPA;所以 hibernate的annotation是JPA标准之下的,一般都直接用…

自己用java实现飞鸽传书 2 - 实现文件传输

第二步:实现文件传递。 上一步只是从服务端传递了一个字符串到客户端,这次需要对代码进行调整,实现从服务端获取文件,在客户端将文件存入目标地址。 调整后的代码: 服务端: import java.io.DataInputStream…

如何理解JavaScript中给变量赋值,是引用还是复制

一、JavaScript中值的类型 JavaScript中的值分为2大类:基本类型和引用类型。每种类型下面又分为5种类型。 基本类型: 数字类型:Number;字符串类型:String;布尔类型:Boolean(true和false)&#x…

CommonCollection1反序列化链学习

Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/course/detail/35475 CommonsCollection1 1、前置知识 1.1、反射基础知识 1.1.1、 对象与类的基础知识 类(class)&am…

【英语天天读】第一场雪

作者:gnuhpc 出处:http://www.cnblogs.com/gnuhpc/ --Henry Wadsworth Longfellow The first snow came. How beautiful it was, falling so silently, all day long, all night long, on the mountains, on the meadows, on the roofs of the living, o…

性能测试的目的与类型

1.性能测试的目的 (1)评估系统的能力:测试中得到的负荷和响应时间数据可以被用于验证所计划的模型的能力,并帮助作出决策;(2)寻找系统瓶颈,进行系统调优;(4)检测软件中的问题;(5)验证稳定性、可靠性&#x…

求三位数的质数

没做出来啊&#xff0c;原来有这么多方法啊。首先&#xff0c;我连质数是什么都不知道&#xff01;质数&#xff1a;只能被本身和1整除的数帖子里回复了不少方法&#xff1a;class Zhishu {public static void main(String[] args) {int count0;for(int i1;i<100;i){count0…

[转]VS2010+MFC解析Excel文件中数据

本文转自&#xff1a;http://www.vcfans.com/2010/08/vs2010-mfc-excel-file-in-the-data-analysis.html 前两天折腾一个小功能&#xff0c;需求是解析Excel中的数据出来。网上一般使用的方案&#xff1a;1. ODBC当数据库来操作。2. 使用第三方的类库3. 使用COM调用Excel.exe中…

MySQL索引机制(详细+原理+解析)

Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/course/detail/35475 MySQL索引机制 永远年轻&#xff0c;永远热泪盈眶 一.索引的类型与常见的操作 前缀索引 MySQL 前缀索引能有效减小索引文…

War-Driving(战争驾驶***)

War-Driving总结性的文章 以后应该不会在到这方面过多的下功夫了。点我下载转载于:https://blog.51cto.com/0x007/1586376

array sort - 4 : merge sort

NULL转载于:https://www.cnblogs.com/roadmap99/p/6698809.html

带研发团队后的日常思考1 初级管理者的困惑

Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/course/detail/35475 带研发团队后的日常思考1 初级管理者的困惑 前言&#xff1a; 本人于2020年4月开始接触管理工作到现在有2年的时间&#…

短连接生成器——让你的url地址长度变短

http://www.henshiyong.com/tools/sina-shorten-url.php 转载于:https://www.cnblogs.com/mangu-uu/archive/2012/10/15/2724290.html

JS函数调用的四种方法

js的函数调用会免费奉送两个而外的参数就是 this 和 arguments 。arguments是参数组&#xff0c;他并不是一个真实的数组&#xff0c;但是可以使用.length方法获得长度。 书上有说4中调用方式&#xff1a; 方法调用模式函数调用模式构造器调用模式apply调用模式下面我们来看看一…

Django项目引入NPM和gulp管理前端资源

Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/course/detail/35475 前言 之前写了一篇《Asp-Net-Core开发笔记&#xff1a;使用NPM和gulp管理前端静态文件》&#xff0c;现在又来用Django开发…

有声听书

各位领导/投资人/用户/合作伙伴&#xff1a; 我们的产品《有声听书》是为了解决中年人&#xff0c;中老年人的痛苦。他们需要丰富生活&#xff0c;但是现有的方案并没有很好地解决这些需求&#xff0c;我们有独特的办法&#xff0c;有戏剧&#xff0c;书的音频能给用户带来好处…

OpenCV笔记(十五)——使用Laplace算子进行图像的边缘检测

在笔记十四中&#xff0c;我们使用了Sobel算子对图像进行边缘检测&#xff0c;理论依据是像素变化最快的地方最有可能是边缘处&#xff0c;所以使用sobel算子对图像做微分&#xff0c;得到的结果图像当中灰度较大的区域&#xff0c;即为边缘处。 在这里&#xff0c;我们使用Lap…

设计模式之:享元模式FlyweightPattern的实现

Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/course/detail/35475 享元模式的理解&#xff1a; 享元模式的定义&#xff1a;运用共享技术支持大量细粒度对象的复用&#xff1b; Flyweight P…

安全公司笔试面试题总结

一IP地址&#xff08;注意地址范围和私有地址的定义&#xff09; IP地址分为五类&#xff0c;A类保留给政府机构&#xff0c;B类分配给中等规模的公司&#xff0c;C类分配给任何需要的人&#xff0c;D类用于组播&#xff0c;E类用于实验&#xff0c;各类可容纳的地址数目不同。…