前面细讲了基于CQS的4层架构,其中的领域模型层也就是六边型架构中的内核在整个开发流程中工作占比最大,也是需要工程师最需要关注地方。那么话说回来了,里面到底包含了什么东西需要投入如此高的关注度。答案还用说?必然是领域模型啊,比如实体、值类型、业务服务等,您别忘了咱们讲的是领域驱动设计。具体可参看如下图所示的领域模型层(后续简称BO层)中的元素。这里面东西较多,基乎每一种都可以开一章来讲,也就是可以水好多的文字。

BO层中元素比较多,但这里面最具特色应该是“业务异常”。您说把领域模型、领域服务归结为BO中的元素这本是无可厚非的,因为它们本来面向的就是业务实体、业务逻辑。把异常也归结为BO中的元素是几个意思呢?而且,书上都没这么讲过,我这是不是故意的哗众取宠骗流量?这话不能这么说,书上没讲过的东西多着呢,人家写书的时候都会站在一定的前提之上,比如读者应该会一门开发语言,应该做过实际的项目,应该有具备哪些基本知识等。咱这种博客什么样的受众都有,内容本身也是个人经验的总结,有点特殊的东西很正常,而且我不仅要讲书上没有的东西,还会讲得很细,您就踏实的看吧。
回到业务异常这个事情上来,在软件的分析设计过程中,那些有形的物体(一般是名词)可以建成实体,比如订单、账户、商品等;还有一些是无形的但在需求过程中也隐晦的提到了,最典型的就是“事件”,这是对动词进行建模的经典案例。而异常则是典型的、经常被隐晦提及的需要被建模的实体,由于其隐蔽性所以在建模时很容易被忽略。举个例子,需求中可能会这样描述:下单失败时应该告知用户失败的原因。这个场景中提及了“失败原因”这个名词,那应该用什么东西来描述它呢?这时就需要业务异常来发挥热量了。显性的提及失败的处理逻辑还好一些,还有一种常见的需求形式比如:下单成功后,给用户发送短信通知。这种需求只描述了正向的业务而没有说明如果失败了要如何处理。此时就需要工程师发挥自己的主动性,结合业务的使用形式为失败的场景建立适宜的处理方式,当然也可以和客户或产品经理就此等情况进行协商,实际的参与到需求完善的过程中。
此种工作方式也应对了我在前面所说过的:软件开发并不是无脑的堆代码。实际上,我也见过一些工程师,当面临业务BUG时候很不愿意承认自己的过失,而是将责任推给需求或其它人,常用的口头语就是“你也没说啊?”,对方的常见回答是“你为什么不问”?然后就开始撕了……客观来讲,我个人比较不喜欢这种工程师,缺少必要的积极性和主动性。工作其实首先成全的是自己,能否让自身成长也只能靠自己,毕竟“师傅领进门,修行在个人”;满足公司的需要这本来就是义务,毕竟你从公司拿钱了;另一方面也需要考虑如何进行自我提升,有时候软技能比会什么什么技术更加重要。我在以往的工作中也曾经历了许多的故事,有些是个人的不足,有些是公司的政治,但一直在努力的进行自我提升包括写这些文章这个事情的本身。不顺是眼前的,有些时候并不是您自身的问题,但能否做一些事情为将来开辟出一条适合自己的路,这是可控的。
有关业务异常还有一些补充,您在日常用的时候务必给他一个见名之意的名字。这么说吧,给异常一个好名儿比您费半天劲想词去描述异常原因更有价值,名起得好在抛异常的时候甚至不用写具体原因。另外,实践中最好让业务异常继承于“Exception”而不是“RuntimeException”,也就是使用检查异常以避免异常未被正确捕获。我写了一个业务异常的示例供参考,注意名字啊,我个人觉得起得挺牛掰的,看名就知道什么错误。另外一点,如果把代码再写细一点,做一个业务异常的基类并让所有的具体异常从它继承,就可以使用一些全局异常处理机制,比在每个方法里做try...catch要强得多。
public class DeploymentApprovalFormCreationException extends Exception {
public DeploymentApprovalFormCreationException() {
super();
}
public DeploymentApprovalFormCreationException(String message) {
super(message);
}
public DeploymentApprovalFormCreationException(String message, Throwable cause) {
super(message, cause);
}
public DeploymentApprovalFormCreationException(Throwable cause) {
super(cause);
}
protected DeploymentApprovalFormCreationException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
除业务异常另外需要着重说明的是“资源仓库接口”。有些工程师会把资源仓库当做DAO来用,这个其实是一种误用。资源仓库要包含哪些接口和您的业务是强关联的,再说直白一点就是由业务来决定资源仓库有哪些能力。两者的目的有着本质的不同:DAO用于操作数据模型,资源仓库用于操作领域模型。系统在实现的时候,领域模型最终还需要变成数据模型才能进行存储和索引,这也是资源仓库该干的事情。
具体来看,领域模型一般会有大量的嵌套对象,各对象间关系复杂,而且也不是所有的对象都需要进行持久化;数据模型相对简单得多,常用的关系型数据库对应的模型也不过是一种二维关系。想要实现两者的转换已经不仅仅是引入一个简单的设计模式比如工厂就能解决的了,还需要再应用一种更为复杂的设计来实现领域模型和数据模型的解耦合,引入“资源仓库”可以达到这个目的;另外,资源仓库还能约束您在设计的时候要以业务模型为驱动以避免陷入面向数据库设计的情况。为了实现洋葱架构的效果,设计时还需要把资源仓库的定义与实现进行分离。由于定义一般是以接口的形式,所以并不会为BO层引入更多的针对基础设施的依赖,不得不感叹DDD的那些先驱还真是聪明。
|
重点! 进行领域模型设计时,需要首先考虑领域模型的实现再决策存储方式。资源仓库的引入可以起到三个用:1)解决领域模型与数据模型间的异构问题,达到转换器的作用;2)提供对领域模型的序列化和反序列化能力的支撑;3)约束您在考虑问题的时候应该使用业务驱动的方式。 |
这里有一个问题,为什么说需要根据业务的需求来设计资源仓库呢?举一个例子,在我们常见的电商购物业务中有“下单”概念,下单的一个重要步骤是对订单模型进行存储,也就说资源仓库应当具备订单持久化的接口;支付后把订单的状态变成已支付,说明还需要有一个从存储中查询订单的能力和变更订单信息的能力,分别对应查询和编辑;订单一般不能删除只能作废说明不需要有删除的需求。针对上述需求,我们发现订单资源仓库应当只有三个接口:1)查询单个订单;2)更新订单;3)存储新订单。具体到查询单个订单的接口,其参数是订单ID还是编号亦或是其它的信息,也是需要根据业务来定的。通过这个案例,相信您应该明白了资源仓库设计的依据是业务而不是数据存储,DAO才是面向数据的。不同的DAO虽然对应的数据模型不一样,但有一些基本的功能是通用的比如:增加、删除、更新等。由于责任单一且不包含业务逻辑,一般都会将DAO作为基础设施层中的组件。
|
重点!
|
资源仓库接口的实现逻辑上属于基础设施层的内容,系统设计过程中我个人一般会将其与基础设施层分开至不同的包中。此外,真实项目中一般也会设计一个资源仓库的基本接口,毕竟大部分场景中都需要对领域模型进行存储、变更和根据ID查询的能力。下面代码展示了两个不同业务的资源仓库接口的定义,您需要注意两点:1)资源仓库接口所在的包应该是BO;2)资源仓库所定义的接口应该由业务来驱动的。
public interface Repository<TID extends Comparable, TEntity extends EntityModel> {
/**
* 根据ID返回领域模型
* @param id 领域模型ID
* @return 领域模型
*/
TEntity findBy(TID id);
/**
* 删除领域实体
* @param entity 待删除的领域实体
*/
void remove(TEntity entity);
/**
* 删除多个领域实体
* @param entities 待删除的领域实体列表
*/
void remove(List<TEntity> entities);
/**
*将领域实体存储至资源仓库中
* @param entity 待存储的领域实体
*/
void add(TEntity entity);
/**
* 将领域实体存储至资源仓库中
* @param entities 待存储的领域实体列表
*/
void add(List<TEntity> entities);
/**
*更新领域实体
* @param entity 待更新的领域实体
*/
void update(TEntity entity);
/**
*更新领域实体
* @param entities 待更新的领域实体列表
*/
void update(List<TEntity> entities);
}
Repository接口是所有资源仓库接口的基类,包含了新建、更新、根据ID查询和删除四类基本操作。有人说资源仓库的接口都应该使用业务术语,类似于“update”、“add”已经偏向于技术,应当使用如“save”代替。我个人觉得这么搞其实挺麻烦的,存储的时候还需要区分到底是插入还是修改,代码会很脏。不过使用业务术语表达每一个接口这个倒是个很重要的规范,您应该遵守。另外有争议的是“delete”接口,这接口其实不应该有,可能也是因为设计时脑子抽了才加上的,您在实践时干掉即可。有了基本接口后,下面就可以基于此来定义业务级资源仓库接口。
package xx.workflow.bo.opresourceapply.repository;
import xx.common.odd.repository.Repository;
import xx.workflow.bo.opresourceapply.OprApplyForm;
public interface OprApplyFormRepository extends Repository<Long, OprApplyForm> {
}
“OprApplyFormRepository”所面向的实体是“OprApplyForm”,其中没有再定义任何其它接口,说明只需要使用“Repository”中的能力即可。
package xx.servicedeployment.bo.repository;
import xx.common.odd.repository.PersistenceException;
import xx.common.odd.repository.Repository;
import xx.servicedeployment.bo.DeploymentDetail;
public interface DeploymentDetailRepository extends Repository<Long, DeploymentDetail> {
/**
* 根据部署审批单查询部署详情
* @param deploymentApprovalFormId 部署审批单ID
* @return 部署详情
*/
DeploymentDetail findByDeploymentApprovalFormId(Long deploymentApprovalFormId) throws PersistenceException;
}
“DeploymentDetailRepository”中多了一个“根据部署审批单查询部署详情”接口,说明某个命令型业务中有需要根据“部署审批单”查询“DeploymentDetail”这个业务实体的需求。其它有关“DeploymentDetail”的能力仍然从基类中继承。
根据上面的演示,您可以看到资源仓库接口的定义遵循了前面所说的全部规范尤其是其所操作的对象都应是领域模型;您应该也看到了类似查询“XXX信息列表”这种单纯用于查询的方法并没有出现在资源仓库接口中。
根据上图所示,您已经明确了BO层所包含的元素的种类。我们前面说BO层很厚,这么多东西都在这个层里,想不厚也不行啊。如果落实到代码中,这些元素一般会统一放到一个包中,包名即为业务名,如下图所示。针对包的组织,我建议这么做:根据业务能力将服务分成几个子BC,以包的形式组织这些BC。比如订单服务中需要包含两项业务:订单管理、发货单生成,那针对这两项分别建立两个包,每个包都按下图所示的结构进行代码组织。不建议建立如BO、DAO、VO、Service四个包,根据这些包对代码进行组织和分类。

上图展示了“审批服务”业务的代码结构,BO这个包中除了事件、业务异常和资源仓库接口,其它的都是实体类型、值类型等组件。根据业务能力组织代码,您会发现即使是一个单体的系统,在遵循了DDD设计规范后仍能具备高内聚的属性,后续如果需要拆分时只需要做一些简单的工作即可。
既然BO层处于系统的核心位置,根据六边型模型的要求就需要在依赖与访问控制两个方面进行约束。依赖相对简单,只要让其别依赖于其它层就OK,也就是限制这层对其它层元素的引用;特别常见的一个错误就是在领域实体中引入资源仓库接口或DAO,虽然初衷是为了提升性能,但会造成代码结构的混乱,损害了代码的健壮性使系统成为了所谓的大泥球(其实球不球的也不是重点,有了BC的隔离最多是个小泥球,胡乱的引用体现出您的工作没有规则)。访问控制方面,针对每个对象的访问级别包括public、package、private等需要进行充分考虑,做到最大化的隔离。除应用服务层和资源仓库实现层,其它层不可以直接引用业务模型。下面的代码展示了BO层中实现领域模型时的反例,供参考。

问题一:业务模型依赖Spring框架;问题二:访问了其它包的DAO;问题三:反向依赖资源仓库,记住:资源仓库实现与领域模型的依赖是单向的。
本章讲解了BO层中所包含的元素,尤其对于业务异常和资源仓库接口进行了重点说明。通过本章的内容相信您已经对于所谓的六边型架构中的内核及其构成有了一个感性的认识,也为后面的学习打下了一定的基础,后续我们会深入到BO内部对各元素逐一的进行解释。
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数
查看我的Ruby代码:h=Hash.new([])h[0]=:word1h[1]=h[1]输出是:Hash={0=>:word1,1=>[:word2,:word3],2=>[:word2,:word3]}我希望有Hash={0=>:word1,1=>[:word2],2=>[:word3]}为什么要附加第二个哈希元素(数组)?如何将新数组元素附加到第三个哈希元素? 最佳答案 如果您提供单个值作为Hash.new的参数(例如Hash.new([]),完全相同的对象将用作每个缺失键的默认值。这就是您所拥有的,那是你不想要的。您可以改用
按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭10年前。问题1)我想知道rubyonrails是否有功能类似于primefaces的gem。我问的原因是如果您使用primefaces(http://www.primefaces.org/showcase-labs/ui/home.jsf),开发人员无需担心javascript或jquery的东西。据我所知,JSF是一个规范,基于规范的各种可用实现,prim
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
本文主要介绍在使用Selenium进行自动化测试或者任务时,对于使用了iframe的页面,如何定位iframe中的元素文章目录场景描述解决方案具体代码场景描述当我们在使用Selenium进行自动化测试的时候,可能会遇到一些界面或者窗体是使用HTML的iframe标签进行承载的。对于iframe中的标签,如果直接查找是无法找到的,会抛出没有找到元素的异常。比如近在咫尺的例子就是,CSDN的登录窗体就是使用的iframe,大家可以尝试通过F12开发者模式查看到的tag_name,class_name,id或者xpath来定位中的页面元素,会抛出NoSuchElementException异常。解决
目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称
项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU
我是HanamiWorld的新人。我已经写了这段代码:moduleWeb::Views::HomeclassIndexincludeWeb::ViewincludeHanami::Helpers::HtmlHelperdeftitlehtml.headerdoh1'Testsearchengine',id:'title'hrdiv(id:'test')dolink_to('Home',"/",class:'mnu_orizontal')link_to('About',"/",class:'mnu_orizontal')endendendendend我在模板上调用了title方法。htm
我在我的项目中有一个用户和一个管理员角色。我使用Devise创建了身份验证。在我的管理员角色中,我没有任何确认。在我的用户模型中,我有以下内容:devise:database_authenticatable,:confirmable,:recoverable,:rememberable,:trackable,:validatable,:timeoutable,:registerable#Setupaccessible(orprotected)attributesforyourmodelattr_accessible:email,:username,:prename,:surname,: