- 浏览: 387557 次
- 性别:
- 来自: 深圳
文章分类
最新评论
-
Nabulio:
写的详细,特殊语法学习到了
jdk1.5-1.9新特性 -
wooddawn:
您好,最近在做个足球数据库系统,用到了betbrain的数据表 ...
javascript深入理解js闭包 -
lwpan:
很受启发 update也可以
mysql 的delete from 子查询限制 -
wuliaolll:
不错,总算找到原因了
mysql 的delete from 子查询限制
领域驱动设计DDD
当我们接到一个新项目时,使用UML工具,通过面向对象DDD的分析设计方法将其变成领域模型图,如下:
这是一个典型的DDD建模图,这个模型图可以直接和Java代码对应,比如其中Cargo模型的代码如下,两者是完全一一对应,可以使用together等建模工具直接转换,Jdon框架的@Model就是针对Cargo这样模型,将其运行在Java平台中,:
package ship;
@Model
public class Cargo {
private String id;
private ship.DeliveryHistory lnkDeliveryHistory;
private ship.DeliverySpec lnkDeliverySpec;
public Cargo(String trackingId, DeliverySpec deliverySpec) {
this.id = trackingId;
this.lnkDeliverySpec = deliverySpec;
}
public void changeDestination(final Location newDestination) {
lnkDeliverySpec.setDestination(newDestination);
}
//跟踪货物位置
public Location lastKnownLocation() {
final HandlingEvent lastEvent = this.getLnkDeliveryHistory().lastEvent();
if (lastEvent != null) {
return lastEvent.getLocation();
} else {
return null;
}
}
//当货物在运输抵达目的地点时
public boolean hasArrived() {
return lnkDeliverySpec.getDestination().equals(lastKnownLocation());
}
//跟踪顾客货物的关键装卸事件
public boolean isUnloadedAtDestination() {
for (HandlingEvent event : this.getLnkDeliveryHistory().eventsOrderedByCompletionTime()) {
if (HandlingEvent.Type.UNLOAD.equals(event.getType())
&& hasArrived()) {
return true;
}
}
return false;
}
……
}
当领域模型Cargo出来以后,下一步就是使用Jdon框架来将其运行起来,因为Jdon框架分为领域模型和组件技术等两个部分,Cargo无疑属于@Model模型架构,我们只要给模型加上@Model,就能让Cargo的对象生活在内存缓存中。
@Model
public class Cargo {
}
为什么需要DDD?
以订单为例子,如果不采取DDD设计,而是通常朴素的数据表库设计,将订单设计为订单数据表,那么带来的问题是:
将实体的职责分离到不同限定场景 ,比如订单中有OrderItemId, OrderId, ProductId 和 Qty,这是合乎逻辑的最初订单,后来有 MinDeliveryQty 和 PendingQty字段,是和订单交货有关,这其实是两个概念,订单和订单的交货,但是我们把这些字段都混合在一个类中了。
混淆在一起的问题就是将来难以应付变化,因为实体的职责是各自变化的。
领域不是把实体看成铁板一块,一开始就把它分解到各种场景。下订单和订单交货交付是两个场景,它们应该有彼此独立的接口,由实体来实现。这就能够让实体和很多场景打交道,而彼此不影响。
DDD和数据库分析设计的区别是:在数据库中它们是一个,也就是说,从ER模型上看,它们是一个整体,但是从领域模型角度看,它们是分离的。
传统架构与DDD脱节
DDD和Spring+Hibernate或JavaEE的架构有什么区别?
其实,现有架构和DDD是脱节,也就是说,现有架构不能很好支撑DDD,为什么这么说呢,因为在这样架构下,DDD实体一直是被操作,作为方法参数传来传去,见下面伪代码演示:
public void myMethod(Entity entity){
.....
}
然后我们会有出现另外一个方法类似上面,只是其中方法代码有稍微不同:
public void myMethod2(Entity entity){
.....
}
public void myMethod3(Entity entity){
.....
}
问题来了,由于这三个方法中有一部分是共同的,当我们修改一个方法时,另外两个都要修改,万一忘记修改,就出现BUG,这时我们很容易想把三个方法中共同部分抽象成一个方法,这里有两个方向:
首先用继承模板,其实这是坏的设计,为什么坏这里不多说。更主要的问题是:我们只是把几个功能类(服务类)合并成一个类,实体还是处于被传入被操作,如果这段操作需要事务,我们只能在这个合并类中加入事务,导致锁粒度扩大。
public class Service{
public void myMethodCommon(Entity entity){
//事务开始
......
//事务结束
}
}
上面这个服务类共同抽象出来的方法内加上事务后,尽管同时操作的是两个不同的实体对象,也会发生排他锁,某个时刻只能有一个实体在这个事务中被操作。
实际上,我们是要根据实体对象来进行事务,只有两个线程同时操作一个实体对象时,我们才需要事务排他锁。
所以,之前我们总是从功能行为角度考虑实现,换个不同角度,从实体角度考虑,这共同的部分是不是可以写入实体内部呢? 也许从业务上讲,属于实体的行为,属于实体的职责,实体应该自己干的事情,应该有责任去做的事情,当然这其中也区分为基本职责和业务职责,后者我们通过DCI来实现。
下面问题来了,实体代码变为:
public class Entity{
public void myMethodCommon(){
......
}
}
如果myMethodCommon方法需要调用存储数据库,或调用服务或调用其他实体交互怎么办?
那么一般就是把这些资源注入到实体中,实体代码为:
public class Entity{
//数据库资源
public MyRepository myRepository;
//其他服务....
public MyService myService;
public void myMethodCommon(){
......
}
}
很明显,架构技术因素侵入搞脏了实体,怎么解决这个问题?
联想到Command命令模式,既然用户通过界面可以向服务以命令模式发出各种调用(如Struts等框架),实体作为用户行中模型的代表,代表用户需求思想,为什么不可以以命令对技术架构发出各种调用呢?通过命令模式,实体将各种调用打包成请求,而我们这时需要做的只是在实体中提供一个传递命令管道:
public class Entity{
public SubPublisherRoleIF subPublisherRole;
public void myMethodCommon(){
......
subPublisherRole.send...
}
}
这样,基本杜绝了实体和技术架构如各种仓储或服务的依赖,当然,更好的方式是,连命令管道都没有,直接在运行是注入各种实体需要的资源,这种方式虽然很干净,好像一点副作用都没有,但是带来另外问题,如变魔术,代码本来是让大家读懂的脚本,这时反而变得不容易理解,因为看到的代码和实际运行是完全不同的。
胖模型
上述将属于实体的行为放入实体内部,但是这样将所有行为都放入实体类中会造成胖模型,实际上这些行为只是实体在不同场景下才具有的行为,就如同你在家里是儿子,有一些儿子角色行为,但是在单位是程序员角色,有编程行为,把在家里和在单位不同场景具有的行为放在一起很显然冗余。
对象的行为分两种:一种是维护对象内部字段也就是状态一致性或完整性的行为,还有一种是与外部交互调用的行为。
面向对象非常擅长显式表达状态,类 字段和属性这些都是用来定义状态的强大工具。(banq注:场景 事件和状态可以认为一个目标模板)。
对象的状态着重于两个方面,一个是编译时期,一个运行时期,在编译时期我们能看到对象的类定义;而在运行时刻我们可以调用对象实例的字段。
对象的行为是围绕本地状态的,不包括那些与外部协调等行为,这些本地行为只围绕对象自己内部状态进行。例如:
public Class Entity{
private int state;
private boolean show;
//这个方法维护state和show之间的逻辑关系,数据一致性。
public void setState(int state){
if (this.state >1 ){
this.state = state;
show = true;
}
}
}
但是OO弱点在于无法表达与外部协调交互collaborations 。看如下两个案例用例(最后两张图),分别表达A B C D四个对象之间的相互调用。第一张图表示在传统OO中,我们可能将这些相互协调调用的方法专门当前类中,导致A B C D很多方法,这样带来问题看下篇:
在用例场景2下,ABCD之间又有如下行为调用:
那么为完成上面两个用例功能,我们已经知道应该将这些行为放入实体内部,因为他们属于实体的行为,但是又属于实体之间相互调用,设计实体类图如图下
上述方式就是造成一些巨无霸的胖模型。
上面将很多交互方法也置于对象中的问题是:编译时刻的代码并不能反映其运行时刻发生的。源码告诉我们有四个分离的对象,分别带有许多方法(messageA0 messageB0....)。而运行时刻告诉我们:有四个对象彼此有许多交互谈话调用,其中只有很少部分和某个特定用例有关(和User Case1相关,或和User Case2相关),因为你将所有用例相关的方法都混淆在一个对象中。
这种不匹配导致程序非常难懂,源码告诉我们一个故事,而运行时效果告诉我们是完全不同的故事。
DCI
通过引入DCI: http://www.jdon.com/tags/10443,数据Data 上下文Context 交互Interaction(DCI):面向对象范式的演进。它是由MVC发明者发明的,它提供了一种分析需求的统一语言,认为领域模型在不同场景下扮演不同角色,因而具有不同的角色行为,那些丰富的行为其实是角色的,在运行时,我们将领域模型和角色集合在一起,就如同你进入家门口一刹那,你这个人和角色儿子就开始绑定,直至离开家这个场景。
这样,上面UserCase1和UserCase2不同用例功能可以看成是Domain Model领域模型在不同场景上下文扮演不同角色发生的行为,如下图表示:
使用DCI的好处:
本地化:
传统OO要完成一个算法过程需要跨不同文件,使用DCI,对于一个特定用例只需要看一个文件即可。
聚焦:场景上下文只包含有对应用例需要的功能方法,你就不必在数百个方法中寻找工作。
实现“系统是什么”和“系统做什么”分离:
系统是什么:系统是什么样的?意思指所有数据对象(领域模型)和他们本地方法,通常这是系统部分最稳定的。
系统做什么:是指不同场景快速改变的行为,将系统稳定部分和经常变化的部分进行分离。DCI提供这种分离。
* DCI的数据类(领域模型)是告诉我们有关系统是什么。
* DCI场景上下文Context告诉我们这些数据类外部相互关系是什么。
角色变得显式:
DCI最伟大的贡献是带来了显式的角色概念,(角色类似服务或事件,是一种跨业务领域和技术架构的桥梁。)比如我出生在俄罗斯,我的名字是Victor;我体重 65kg. 这些属性会影响一些高层次职责吗?当我回到家我扮演丈夫角色,我在单位扮演经理角色,等等。
这表明,角色没有在传统OO中成为第一等公民是错误的。
场景 事件与状态
上面DCI帮助我们减轻了胖模型可能,那么在具体实施又带来一个问题,领域模型注入到角色中,还是领域模型主动扮演角色呢?如果是前者,这和传统SOA架构中,使用服务来调用领域模型非常类似,这时领域模型只是被操作,而没有发挥主动的支配地位。
另外从代码可读性上看,要了解一个领域模型扮演了多少角色也不是一件轻松的事情,如果我们在一个领域模型中能看到其所有可能扮演的角色,这样对象阅读将非常方便。
那么领域模型如何发出指令呢?通过事件和消息。
在MartinFowler的Event Sourcing文章谈到,将导致状态变化的所有事件提取出来
以货运为案例:
原来架构是一个服务和一个实体,引入事件后:
那么正如大部分系统都可以引入服务这个概念,“服务”这个概念既是业务也是技术概念,成为业务和技术架构的桥梁,谈到服务,业务人员知道它在业务上代表什么,技术人员知道如何实现它。
而“事件”也是一种和“服务”类似的介于技术和业务之间桥梁的术语,通过引入事件,应该不会对业务领域包括实体有伤害作用,也不会感觉它是天外来客或神仙姐姐,否则服务这个概念也应该是了。
统一语言
1. 什么是统一语言?
DDD强调统一语言,所谓统一语言,也就是统一需求人员和技术人员的用语,不能各用一套自己的定义,这样就会鸡同鸭讲,需求和技术脱节,导致软件实现错误需求,或者无法跟随需求变化而变化,这类似一种电视游戏:拷贝不走样,几个人通过动作模仿传递一个成语,结果到最后一个人得出的结论完全相反,这是反映信息在传播中的失真。所以,建议统一语言,能够让大家用同一种描述方式来谈论问题。
2. 统一语言是什么样?
统一语言有不同流派,比如最早的SOA的服务概念也是一种统一语言,所以,我们做系统项目时,总是有服务类,而在现实中也存在各种从服务角度描述的需求,比如加油站加油服务等等。
服务这个统一语言,需求人员能够理解;技术人员也能够理解,不就是一个无状态的功能类嘛。
同样,EventSource提出事件和服务一样,也是一种统一语言,虽然案例中是以货运为案例,但是只要有服务的地方就有事件,为什么这么推理呢?
从本站以前一直讨论的四色原型也就是彩色UML,它认为需求世界分为四种颜色,就像我们认为颜色是由三原色组成一样,这四色有角色 活动 和事情和描述。
其中活动这一分类可以对应到服务或者事件,活动发生,也就是事件发生,角色可以认为和场景Context有关。
好,下面再看看其他流派,比如BDD行为驱动开发,其Given When Then模板实际也是一种统一语言,它认为任何用例需求可以用这种模板去分解。所以,统一语言实际是一种方法论。
再看看DCI,它认为领域模型总是在一定场景下扮演角色,实施一定的行为(或发生什么事情),普通平民(领域模型)充当恐怖分子角色,实施爆炸事件。
从列举法来看,由ES/彩色UML或DCI等等都不约而同谈到活动事件,我由此统一总结为:场景(角色) 事件和状态为一个万能模板,是一种统一语言。
3. 统一语言是横跨业务领域和技术架构
由于业务领域和技术架构是天然两个世界,所以,从技术架构内部无论寻找什么超酷的技术都无法和业务领域无缝结合,反之,我们也不可只从某个具体业务领域内部找一个和技术架构无缝衔接。
只有寻找一种凌驾于业务领域和技术架构的,类似神仙姐姐的统一语言,通过这个神仙桥梁,搭建起业务领域和技术架构无缝结合。
那么是否有必要使用CQRS和ES呢?那看你们团队善于掌握哪种统一语言,如果都熟悉服务,那么SOA也许合适,但是它也有缺陷,忽视领域模型的地位了。
如果熟悉事件,当然ES和CQRS是选择,而且事件是围绕DDD实体的,这和DDD比较接近。
如果熟悉场景角色,DCI是一种选择,这也是围绕DDD实体的。
Jdon框架推崇DDD+DCI+ES/CQRS。
JdonFramework = DDD + DCI + ES/CQRS
因为DCI和ES虽然正交,但是不矛盾,DDD的实体充当DCI的数据模型,DCI的Context是角色扮演场所,角色实施的行为是事件,事件由角色在一定场景下发出。
Jdon框架默认编程统一语言是:
由领域模型(DDD)扮演的角色(Role of DCI)在用例场景(Context of DCI)下触发了一个交互事件(interaction event)。
为什么需要事件?
以货运为案例,如下图:
有些人会误解ShoppingEvent为实体,事件如果是一个实体,变成事件的记录了(事件变成了状态),并不能代表正在发生的事件。
而我们根据前面统一语言,设计如下:
public Class Ship{
@Inject
ShippingRole shippingRole;
.....
}
Ship是data model也就是领域模型 ,ShippingRole是角色,由角色ShippingRole发出各种装船事件 比如到达 卸货等事件。
有些DCI实现是将Ship注入到ShippingRole中,我个人认为对阅读代码理解不利,你要了解领域模型的所有功能,必须一个个找它被注入到了那些角色场景中,这和SOA下被服务类操作使用有什么区别呢?
将领域模型的所有可能扮演的角色陈列在领域模型的,而角色的行为都分离到角色中,这样有助于我们只要打开领域模型类的代码,大概知道这个模型实现哪些功能?
以上是从业务角色看实现的ShippingRole角色,那么从技术架构角度看,领域模型还有持久化自己等等,按照职责驱动开发方法,无论是业务要求或技术要求,这些都是领域模型应该干的事情,应该承担的职责,难道业务上干的事情在领域模型中,技术要要求干的就不让领域模型干了?
由此,我们从对象职责这个角度综合业务和技术要求,得出领域模型如下:
public Class Ship{
@Inject
LazyOperatorRole lazyOperatorRole;
@Inject
ShippingRole shippingRole;
.....
}
其中LazyOperatorRole角色负责对实体Ship的其他次要字段根据需要进行加载,或者根据需要进行数据库持久保存。
其实,这里LazyOperatorRole和ShippingRole也是DDD中强调的Bounded Context,因为谈到Context,总是相伴角色的,比如DCI中谈到Context,隐式地其实围绕角色Role。
我们在实体中通过引入符合一定BoundedContext的角色,将这些BoundedContext下发生的行为分离进入角色,由角色通过发送事件给外界,如果不使用事件发送,这些角色行为实现不管是业务行为还是技术行为,总是不可预期的,它们可能是技术和业务混合在一起调用,比如查个数据库,发邮件,关联一下字段等等,这些复杂行为反而会弄脏领域模型,而只有通过事件引入,才能分离领域模型和其生存的环境。
所以,事件的引入已经不只是对业务有好处(业务本身就有事件如shipping事件),而且对架构有好处(如存储事件),更主要保证领域模型不会被太多细节包括技术细节覆盖,突出显示其作为系统核心,其代表用户需求的中心地位。
面向DDD的事件驱动架构
为了更好地突出应用需求为主,Jdon框架采取面向DDD的主要设计思想,主要特点是常驻内存in-memory的领域模型向技术架构发送事件消息,驱动技术架构为之服务,如同人体的DNS是人体各种活动的主要控制者和最高司令,领域模型是一个软件系统的最高司令。
JF有五种模型组件,如下:
一. 实体聚合根对象 元注释 @Model;
二. 服务Service 元注释 @Service;
三. 普通类组件构件 @Component;
四. 生产者Prodcuer-消费者模型 @send @Consumer;
五. 拦截器 @ Interceptor, 首先需要导入点 @Introduce;
所有都在 com.jdon.annotation.*包中。
这些模型组件其实有划分为两大类:业务和技术:
常驻内存@Model 领域模型,包括实体模型 值对象和领域服务,与技术架构无关。相当于鱼;生存空间是缓冲器中
@Component 技术组件架构,用以支撑领域模型在计算机系统运行的支撑环境,相当于鱼生活的水。空间在Context container,例如ServletContext中。
两者以Domain Events模式交互方式: 领域模型向技术组件发出异步命令。
实体模型
根据DDD方法,需求模型分为实体模型 值对象和领域服务三种,实际需求经常被划分为不同的对象群,如Cargo对象群就是Cargo为根对象,后面聚合了一批与其联系非常紧密的子对象如值对象等,例如轿车为根对象,而马达则是轿车这个对象群中的一个实体子对象。
在Jdon框架中,实体根模型通常以@Model标识,并且要求有一个唯一的标识主键,你可以看成和数据表的主键类似,它是用来标识这个实体模型为唯一的标志,也可以使用Java对象的HashCode来标识。
Jdon框架是实体模型的主键标识有两个用处:
首先是用来缓存实体模型,目前缓冲器是使用EHcache,可以无缝整合分布式云缓存Terracotta来进行伸缩性设计。
只要标识@Model的实体,在表现层Jsp页面再次使用时,将自动直接从缓存中获得,因为在中间业务层和表现层之间Jdon框架存在一个缓存拦截器CacheInterceptor,见框架的aspect.xml中配置。
为了能够在业务层能够使用缓存中的模型,需要在业务层后面的持久层或Repository中手工加入缓存的Annotation标签,如下:
实体模型的缓存架构如下:
注意:这里可能是Jdon框架 6.2的一个创新或重要点,在JF6.2之前实际上没有对模型进行突出的支持,就象画个圈,圈子外面基本都就绪,圈子里面留白,这个圈子就是Domain Model,原来因为考虑持久层Hibernate等ORM巨大影响力,就连Spring也是将Model委托给Hibernate处理,自己不做模型层,但是当NoSQL运动蓬勃发展,DDD深入理解,6.2则找到一个方式可以介入模型层,同时又不影响任一持久层框架的接入。
Jdon框架6.2通过在持久层的获得模型对象方法上加注释的办法,既将模型引入了内存缓存,又实现了向这个模型注射必要的领域事件Domain Events。
事件Event Sourcing
JF的最大特点是领域模型驱动技术架构;
如果说普通编程缺省是顺序运行,那么事件模型缺省是并行运行,两者有基本思路的不同。
Event Sourcing适合将复杂业务领域和复杂技术架构实现分离的不二之选。实现业务和技术的松耦合,业务逻辑能够与技术架构解耦,将”做什么”和”怎么做”分离
事件模型也是一种EDA架构Event-driven Architecture,可以实现异步懒惰加载Asynchronous Lazy-load类似函数式语言的懒功能,只有使用时才真正执行。
具有良好的可伸缩性和可扩展性,通过与JMS等消息系统结合,可以在多核或多个服务器之间弹性扩展。
事件模型也是适合DDD落地的最佳解决方案之一。领域模型是富充血模型,类似人类的DNA,是各种重要事件导向的开关。用户触发事件,事件直接激活领域模型的方法函数,再通过异步事件驱动技术活动,如存储数据库或校验信用卡有效性等。
2009年Jdon框架6.2就推出了Domain Model + In-memory + Events.,2001年Martin fowler在其文章LMAX架构 推荐In-memory + Event Sourcing架构。以下是该文的精选摘要,从一个方面详细说明了事件模型的必要性:
内存中的领域模型处理业务逻辑,产生输出事件,整个操作都是在内存中,没有数据库或其他持久存储。将所有数据驻留在内存中有两个重要好处:首先是快,没有IO,也没有事务,其次是简化编程,没有对象/关系数据库的映射,所有代码都是使用Java对象模型。
使用基于内存的模型有一个重要问题:万一崩溃怎么办?电源掉电也是可能发生的,“事件”(Event Sourcing )概念是问题解决的核心,业务逻辑处理器的状态是由输入事件驱动的,只要这些输入事件被持久化保存起来,你就总是能够在崩溃情况下,根据事件重演重新获得当前状态。
事件方式是有价值的因为它允许处理器可以完全在内存中运行,但它有另一种用于诊断相当大的优势:如果出现一些意想不到的行为,事件副本们能够让他们在开发环境重放生产环境的事件,这就容易使他们能够研究和发现出在生产环境到底发生了什么事。
这种诊断能力延伸到业务诊断。有一些企业的任务,如在风险管理,需要大量的计算,但是不处理订单。一个例子是根据其目前的交易头寸的风险状况排名前20位客户名单,他们就可以切分到复制好的领域模型中进行计算,而不是在生产环境中正在运行的领域模型,不同性质的领域模型保存在不同机器的内存中,彼此不影响。
LMAX团队同时还推出了开源并发框架Disruptor,他们通过测试发现通常的并发模型如Actor模型是有瓶颈的,所以他们采取Disruptor这样无锁框架,采取了配合多核CPU高速缓冲策略,而其他策略包括JDK一些带锁都是有性能陷阱的: JVM伪共享。
JF的领域事件是基于号称最快的并发框架Disruptor开发的,因此JF的事件是并行并发模型,不同于普通编程是同一个线程内的顺序执行模型。
JF的领域事件是一种异步模式 + 生产者-消费者模式。主题topic和Queue队列两种。领域模型是生产者;消费者有两种:
.@Consumer;可以实现1:N多个,内部机制使用号称最快的并发框架Disruptor实现。适合轻量;小任务;原子性;无状态。
.@Componet;直接使用普通组件类作为消费者,使用jdk future机制,只能1:1,适合大而繁重的任务,有状态,单例。
Domain Events实现机制如下:
JF的事件模型还是一种CQRS架构,可以实现模型的修改和查询分离,也就是读写分离的架构:
无堵塞的并发编程
顺序编程和并发编程是两种完全不同的编程思路,堵塞Block是顺序编程的家常便饭,常常隐含在顺序过程式编程中难以发现,最后,成为杀死系统的罪魁祸首;但是在并发编程中,堵塞却成为一个目标非常暴露的敌人,堵塞成为并发不可调和绝对一号公敌。
因为无堵塞所以快,这已经成为并发的一个基本特征。
过去我们都习惯了在一个线程下的顺序编程,比如,我们写一个Jsp(PHP或ASP)实际都是在一个线程
下运行,以google的adsense.Jsp为例子:
<%
//1.获得当前时间
long googleDt = System.currentTimeMillis();
//2.创建一个字符串变量
StringBuilder googleAdUrlStr = new StringBuilder(PAGEAD);
//3.将当前时间加入到字符串中
googleAdUrlStr.append("&dt=").append(googleDt);
//4.以字符串形成一个URL
URL googleAdUrl = new URL(googleAdUrlStr.toString());
%>
以上JSP中4步语句实际是在靠一个线程依次顺序执行的,如果这四步中有一步执行得比较慢,也就是我们所称的是堵塞,那么无疑会影响四步的最后执行时间,这就象乌龟和兔子过独木桥,整体效能将被跑得慢的乌龟降低了。
过去由于是一个CPU处理指令,使得顺序编程成为一种被迫的自然方式,以至于我们已经习惯了顺序运行的思维;但是如今是双核或多核时代,我们为什么不能让两个CPU或多个CPU同时做事呢?
如果两个CPU同时运行上面代码会有什么结果?首先,我们要考虑两个CPU是否能够同时运行这段逻辑呢?
考虑到第三步是依赖于前面两步,而第二步是不依赖第一步的,因此,第一步和第二步可以交给两个CPU同时去执行,然后在第三步这里堵塞等待,将前面两步运行的结果在此组装。
很显然,这四步中由于第三步的堵塞等待,使得两个CPU并行计算汇聚到这一步又变成了瓶颈,从而并不能充分完全发挥两个CPU并行计算的性能。
我们把这段JSP的第三步代码堵塞等待看成是因为业务功能必须要求的顺序编程,无论前面你们如何分开跑得快,到这里就必须合拢一个个过独木桥了。
但是,在实际技术架构中,我们经常也会因为非业务原因设置人为设置各种堵塞等待,这样的堵塞就成为并行的敌人了,比如
我们经常有(特别是Socket读取)
While(true){
……
}
这样的死循环,无疑这种无限循环是一种堵塞,非常耗费CPU,它也无疑成为并行的敌人。
比如JDK中java.concurrent. BlockingQueue LinkedBlockingQueue,也是一种堵塞式的所谓并行包,这些并行功能必须有堵塞存在的前提下才能完成并行运行,很显然是一种伪并行。
由于各种技术上的堵塞存在,包括多线程中锁机制,也是一种堵塞,因为锁机制在某个时刻只允许一个线程进行修改操作,所以,并发框架Disruptor可以自豪地说:无锁,所以很快。
现在非常流行事件编程模型,如Event Sourcing或Domain Events或Actor等等,事件编程实际是一种无堵塞的并行编程,因为事件这个词语本身有业务模型上概念,也有技术平台上的一个规范,谈到事件,领域专家明白如同电话铃事件发生就要接,程序员也能明白只要有事件,CPU就要立即处理(特别是紧急事件),而且事件发生在业务上可能是随机的,因此事件之间难以形成互相依赖,这就不会强迫技术上发生前面Jsp页面的第三步堵塞等待。
因此,在事件模型下,缺省编程思维习惯是并发并行的,如果说过去我们缺省的是进行一个线程内的顺序编程,那么现在我们是多线程无锁无堵塞的并发编程,这种习惯的改变本身也是一种思维方式的改变。
在缺省大多数是并发编程的情况下,我们能将业务上需要的顺序执行作为一种特例认真小心对待,不要让其象癌细胞一样扩散。我们发现这种业务上的顺序通常表现为一种高一致性追求,更严格的一种事务性,每一步依赖上一步,下一步失败,必须将上一步回滚,这种方式是多核CPU克星,也是分布式并行计算的死穴。值得庆幸的是这种高一致性的顺序编程在大部分系统中只占据很小一部分,下图是电子商务EBay将他们的高一致性局限在小部分示意图:
由此可见,过去我们实现的顺序编程,实际上是我们把一种很小众的编程方式进行大规模推广,甚至作为缺省的编程模式,结果导致CPU闲置,吞吐量上不去同时,CPU负载也上不去,CPU出工不出力,如同过去计划经济时代的人员生产效率。
据统计,在一个堵塞或有锁的系统中, 等待线程将会闲置,这会消耗很多系统资源。消耗资源的公式如下:
闲置的线程数 =(arrival_rate * processing_time)
如果arrival_rate(访问量达)很高,闲置线程数将会很大。堵塞系统是在一个没有效率的方式下运行,无法通过提高CPU负载获得高吞吐量。
避免锁或同步锁有多种方式,volatile是一种方式,主要的不变性设计,比如设计成DDD的值对象那种,要变就整个更换,就不存在对值对象内部共享操作;还有克隆原型,比如模型对象克隆自己分别传给并发的多个事件处理;用Visibility 使资料对所有线程可见等等;
最彻底的方式就是使用无锁Queue队列的事件或消息模型,Disruptor采取的是一个类似左轮手qiang圆环RingBuffer设计,见http://www.jdon.com/jivejdon/thread/42466,这样既能在两个线程之间共享状态,又实现了无锁,比较巧妙,业界引起震动。
当然Scala的那种Share nothing的Actor模型也是一种无锁并发模型,不过奇怪的是,发明Disruptor的LMAX团队首先使用过Actor模型,但是并发测试发现还是有性能瓶颈的,所以,他们才搞了一个Disruptor。
领域模型发出事件,事件由Disruptor的RingBuffer传递给另外一个线程(事件消费者);实现两个线程之间数据传递和共享。
当事件消费者线程处理完毕,将结果放入另外一个RingBuffer,供原始线程(事件生产者)读取或堵塞读取,是否堵塞取决于是否需要顺序了。
非堵塞并发使用模式如下:
发出调用以后不必立即使用结果,直至以后需要时才使用它。发出调用后不要堵塞在原地等待处理结果,而是去做其他事情,直至你需要使用这个处理结果时才去获取结果。
被调用者将会返回一个“future”句柄,调用者能够获得这个句柄后做其他事情,其他事情处理完毕再从这个句柄中获得被调用者的处理结果。
JF 6.5版本为推广适合多核CPU的无堵塞并发编程范式进行了探索,使用了Domain Events和DCI等不同抽象层次对并发编程进行了封装,从而降低开发者使用并发编程的难度。
DCI架构
DCI:数据Data, 上下文(场景)Context, 交互Interactions是由MVC发明者Trygve Reenskaug发明的。其核心思想是:
让我们的核心模型更加简单,只有数据和基本行为。业务逻辑等交互行为在角色模型中在运行时的场景,将角色的交互行为注射到数据中。
Jdon框架提供了两种DCI风格实现:一种是将DomainEvents对象注射进入当前模型;还有一种是直接将数据模型和接口混合。这两种方式适合不同场景。
如果我们已经Hold住了一个领域对象,那么就直接通过其发出领域事件实现功能;比如模型的修改。在这种模式下,发出领域事件的领域模型本身已经隐含了场景。事件代表场景出头牵线。
否则,我们显式创建一个上下文Context, 在其中通过RoleAssigner将角色接口注入到领域对象中。比如模型新增创建或删除。(对象不能自己创建自己)。
具体实现可见下面专门DCI案例章节。
依赖注入DI
(一)@Model:模型中可以通过字段的@Inject将其他类注射进入,包括@Component类。被注射的类如果有@Introduce,将再次引入拦截器。
(二)@Component:技术架构中组件可以通过构造器直接注射,被注射的类如果有@Introduce,将再次引入拦截器。
当我们接到一个新项目时,使用UML工具,通过面向对象DDD的分析设计方法将其变成领域模型图,如下:
这是一个典型的DDD建模图,这个模型图可以直接和Java代码对应,比如其中Cargo模型的代码如下,两者是完全一一对应,可以使用together等建模工具直接转换,Jdon框架的@Model就是针对Cargo这样模型,将其运行在Java平台中,:
package ship;
@Model
public class Cargo {
private String id;
private ship.DeliveryHistory lnkDeliveryHistory;
private ship.DeliverySpec lnkDeliverySpec;
public Cargo(String trackingId, DeliverySpec deliverySpec) {
this.id = trackingId;
this.lnkDeliverySpec = deliverySpec;
}
public void changeDestination(final Location newDestination) {
lnkDeliverySpec.setDestination(newDestination);
}
//跟踪货物位置
public Location lastKnownLocation() {
final HandlingEvent lastEvent = this.getLnkDeliveryHistory().lastEvent();
if (lastEvent != null) {
return lastEvent.getLocation();
} else {
return null;
}
}
//当货物在运输抵达目的地点时
public boolean hasArrived() {
return lnkDeliverySpec.getDestination().equals(lastKnownLocation());
}
//跟踪顾客货物的关键装卸事件
public boolean isUnloadedAtDestination() {
for (HandlingEvent event : this.getLnkDeliveryHistory().eventsOrderedByCompletionTime()) {
if (HandlingEvent.Type.UNLOAD.equals(event.getType())
&& hasArrived()) {
return true;
}
}
return false;
}
……
}
当领域模型Cargo出来以后,下一步就是使用Jdon框架来将其运行起来,因为Jdon框架分为领域模型和组件技术等两个部分,Cargo无疑属于@Model模型架构,我们只要给模型加上@Model,就能让Cargo的对象生活在内存缓存中。
@Model
public class Cargo {
}
为什么需要DDD?
以订单为例子,如果不采取DDD设计,而是通常朴素的数据表库设计,将订单设计为订单数据表,那么带来的问题是:
将实体的职责分离到不同限定场景 ,比如订单中有OrderItemId, OrderId, ProductId 和 Qty,这是合乎逻辑的最初订单,后来有 MinDeliveryQty 和 PendingQty字段,是和订单交货有关,这其实是两个概念,订单和订单的交货,但是我们把这些字段都混合在一个类中了。
混淆在一起的问题就是将来难以应付变化,因为实体的职责是各自变化的。
领域不是把实体看成铁板一块,一开始就把它分解到各种场景。下订单和订单交货交付是两个场景,它们应该有彼此独立的接口,由实体来实现。这就能够让实体和很多场景打交道,而彼此不影响。
DDD和数据库分析设计的区别是:在数据库中它们是一个,也就是说,从ER模型上看,它们是一个整体,但是从领域模型角度看,它们是分离的。
传统架构与DDD脱节
DDD和Spring+Hibernate或JavaEE的架构有什么区别?
其实,现有架构和DDD是脱节,也就是说,现有架构不能很好支撑DDD,为什么这么说呢,因为在这样架构下,DDD实体一直是被操作,作为方法参数传来传去,见下面伪代码演示:
public void myMethod(Entity entity){
.....
}
然后我们会有出现另外一个方法类似上面,只是其中方法代码有稍微不同:
public void myMethod2(Entity entity){
.....
}
public void myMethod3(Entity entity){
.....
}
问题来了,由于这三个方法中有一部分是共同的,当我们修改一个方法时,另外两个都要修改,万一忘记修改,就出现BUG,这时我们很容易想把三个方法中共同部分抽象成一个方法,这里有两个方向:
首先用继承模板,其实这是坏的设计,为什么坏这里不多说。更主要的问题是:我们只是把几个功能类(服务类)合并成一个类,实体还是处于被传入被操作,如果这段操作需要事务,我们只能在这个合并类中加入事务,导致锁粒度扩大。
public class Service{
public void myMethodCommon(Entity entity){
//事务开始
......
//事务结束
}
}
上面这个服务类共同抽象出来的方法内加上事务后,尽管同时操作的是两个不同的实体对象,也会发生排他锁,某个时刻只能有一个实体在这个事务中被操作。
实际上,我们是要根据实体对象来进行事务,只有两个线程同时操作一个实体对象时,我们才需要事务排他锁。
所以,之前我们总是从功能行为角度考虑实现,换个不同角度,从实体角度考虑,这共同的部分是不是可以写入实体内部呢? 也许从业务上讲,属于实体的行为,属于实体的职责,实体应该自己干的事情,应该有责任去做的事情,当然这其中也区分为基本职责和业务职责,后者我们通过DCI来实现。
下面问题来了,实体代码变为:
public class Entity{
public void myMethodCommon(){
......
}
}
如果myMethodCommon方法需要调用存储数据库,或调用服务或调用其他实体交互怎么办?
那么一般就是把这些资源注入到实体中,实体代码为:
public class Entity{
//数据库资源
public MyRepository myRepository;
//其他服务....
public MyService myService;
public void myMethodCommon(){
......
}
}
很明显,架构技术因素侵入搞脏了实体,怎么解决这个问题?
联想到Command命令模式,既然用户通过界面可以向服务以命令模式发出各种调用(如Struts等框架),实体作为用户行中模型的代表,代表用户需求思想,为什么不可以以命令对技术架构发出各种调用呢?通过命令模式,实体将各种调用打包成请求,而我们这时需要做的只是在实体中提供一个传递命令管道:
public class Entity{
public SubPublisherRoleIF subPublisherRole;
public void myMethodCommon(){
......
subPublisherRole.send...
}
}
这样,基本杜绝了实体和技术架构如各种仓储或服务的依赖,当然,更好的方式是,连命令管道都没有,直接在运行是注入各种实体需要的资源,这种方式虽然很干净,好像一点副作用都没有,但是带来另外问题,如变魔术,代码本来是让大家读懂的脚本,这时反而变得不容易理解,因为看到的代码和实际运行是完全不同的。
胖模型
上述将属于实体的行为放入实体内部,但是这样将所有行为都放入实体类中会造成胖模型,实际上这些行为只是实体在不同场景下才具有的行为,就如同你在家里是儿子,有一些儿子角色行为,但是在单位是程序员角色,有编程行为,把在家里和在单位不同场景具有的行为放在一起很显然冗余。
对象的行为分两种:一种是维护对象内部字段也就是状态一致性或完整性的行为,还有一种是与外部交互调用的行为。
面向对象非常擅长显式表达状态,类 字段和属性这些都是用来定义状态的强大工具。(banq注:场景 事件和状态可以认为一个目标模板)。
对象的状态着重于两个方面,一个是编译时期,一个运行时期,在编译时期我们能看到对象的类定义;而在运行时刻我们可以调用对象实例的字段。
对象的行为是围绕本地状态的,不包括那些与外部协调等行为,这些本地行为只围绕对象自己内部状态进行。例如:
public Class Entity{
private int state;
private boolean show;
//这个方法维护state和show之间的逻辑关系,数据一致性。
public void setState(int state){
if (this.state >1 ){
this.state = state;
show = true;
}
}
}
但是OO弱点在于无法表达与外部协调交互collaborations 。看如下两个案例用例(最后两张图),分别表达A B C D四个对象之间的相互调用。第一张图表示在传统OO中,我们可能将这些相互协调调用的方法专门当前类中,导致A B C D很多方法,这样带来问题看下篇:
在用例场景2下,ABCD之间又有如下行为调用:
那么为完成上面两个用例功能,我们已经知道应该将这些行为放入实体内部,因为他们属于实体的行为,但是又属于实体之间相互调用,设计实体类图如图下
上述方式就是造成一些巨无霸的胖模型。
上面将很多交互方法也置于对象中的问题是:编译时刻的代码并不能反映其运行时刻发生的。源码告诉我们有四个分离的对象,分别带有许多方法(messageA0 messageB0....)。而运行时刻告诉我们:有四个对象彼此有许多交互谈话调用,其中只有很少部分和某个特定用例有关(和User Case1相关,或和User Case2相关),因为你将所有用例相关的方法都混淆在一个对象中。
这种不匹配导致程序非常难懂,源码告诉我们一个故事,而运行时效果告诉我们是完全不同的故事。
DCI
通过引入DCI: http://www.jdon.com/tags/10443,数据Data 上下文Context 交互Interaction(DCI):面向对象范式的演进。它是由MVC发明者发明的,它提供了一种分析需求的统一语言,认为领域模型在不同场景下扮演不同角色,因而具有不同的角色行为,那些丰富的行为其实是角色的,在运行时,我们将领域模型和角色集合在一起,就如同你进入家门口一刹那,你这个人和角色儿子就开始绑定,直至离开家这个场景。
这样,上面UserCase1和UserCase2不同用例功能可以看成是Domain Model领域模型在不同场景上下文扮演不同角色发生的行为,如下图表示:
使用DCI的好处:
本地化:
传统OO要完成一个算法过程需要跨不同文件,使用DCI,对于一个特定用例只需要看一个文件即可。
聚焦:场景上下文只包含有对应用例需要的功能方法,你就不必在数百个方法中寻找工作。
实现“系统是什么”和“系统做什么”分离:
系统是什么:系统是什么样的?意思指所有数据对象(领域模型)和他们本地方法,通常这是系统部分最稳定的。
系统做什么:是指不同场景快速改变的行为,将系统稳定部分和经常变化的部分进行分离。DCI提供这种分离。
* DCI的数据类(领域模型)是告诉我们有关系统是什么。
* DCI场景上下文Context告诉我们这些数据类外部相互关系是什么。
角色变得显式:
DCI最伟大的贡献是带来了显式的角色概念,(角色类似服务或事件,是一种跨业务领域和技术架构的桥梁。)比如我出生在俄罗斯,我的名字是Victor;我体重 65kg. 这些属性会影响一些高层次职责吗?当我回到家我扮演丈夫角色,我在单位扮演经理角色,等等。
这表明,角色没有在传统OO中成为第一等公民是错误的。
场景 事件与状态
上面DCI帮助我们减轻了胖模型可能,那么在具体实施又带来一个问题,领域模型注入到角色中,还是领域模型主动扮演角色呢?如果是前者,这和传统SOA架构中,使用服务来调用领域模型非常类似,这时领域模型只是被操作,而没有发挥主动的支配地位。
另外从代码可读性上看,要了解一个领域模型扮演了多少角色也不是一件轻松的事情,如果我们在一个领域模型中能看到其所有可能扮演的角色,这样对象阅读将非常方便。
那么领域模型如何发出指令呢?通过事件和消息。
在MartinFowler的Event Sourcing文章谈到,将导致状态变化的所有事件提取出来
以货运为案例:
原来架构是一个服务和一个实体,引入事件后:
那么正如大部分系统都可以引入服务这个概念,“服务”这个概念既是业务也是技术概念,成为业务和技术架构的桥梁,谈到服务,业务人员知道它在业务上代表什么,技术人员知道如何实现它。
而“事件”也是一种和“服务”类似的介于技术和业务之间桥梁的术语,通过引入事件,应该不会对业务领域包括实体有伤害作用,也不会感觉它是天外来客或神仙姐姐,否则服务这个概念也应该是了。
统一语言
1. 什么是统一语言?
DDD强调统一语言,所谓统一语言,也就是统一需求人员和技术人员的用语,不能各用一套自己的定义,这样就会鸡同鸭讲,需求和技术脱节,导致软件实现错误需求,或者无法跟随需求变化而变化,这类似一种电视游戏:拷贝不走样,几个人通过动作模仿传递一个成语,结果到最后一个人得出的结论完全相反,这是反映信息在传播中的失真。所以,建议统一语言,能够让大家用同一种描述方式来谈论问题。
2. 统一语言是什么样?
统一语言有不同流派,比如最早的SOA的服务概念也是一种统一语言,所以,我们做系统项目时,总是有服务类,而在现实中也存在各种从服务角度描述的需求,比如加油站加油服务等等。
服务这个统一语言,需求人员能够理解;技术人员也能够理解,不就是一个无状态的功能类嘛。
同样,EventSource提出事件和服务一样,也是一种统一语言,虽然案例中是以货运为案例,但是只要有服务的地方就有事件,为什么这么推理呢?
从本站以前一直讨论的四色原型也就是彩色UML,它认为需求世界分为四种颜色,就像我们认为颜色是由三原色组成一样,这四色有角色 活动 和事情和描述。
其中活动这一分类可以对应到服务或者事件,活动发生,也就是事件发生,角色可以认为和场景Context有关。
好,下面再看看其他流派,比如BDD行为驱动开发,其Given When Then模板实际也是一种统一语言,它认为任何用例需求可以用这种模板去分解。所以,统一语言实际是一种方法论。
再看看DCI,它认为领域模型总是在一定场景下扮演角色,实施一定的行为(或发生什么事情),普通平民(领域模型)充当恐怖分子角色,实施爆炸事件。
从列举法来看,由ES/彩色UML或DCI等等都不约而同谈到活动事件,我由此统一总结为:场景(角色) 事件和状态为一个万能模板,是一种统一语言。
3. 统一语言是横跨业务领域和技术架构
由于业务领域和技术架构是天然两个世界,所以,从技术架构内部无论寻找什么超酷的技术都无法和业务领域无缝结合,反之,我们也不可只从某个具体业务领域内部找一个和技术架构无缝衔接。
只有寻找一种凌驾于业务领域和技术架构的,类似神仙姐姐的统一语言,通过这个神仙桥梁,搭建起业务领域和技术架构无缝结合。
那么是否有必要使用CQRS和ES呢?那看你们团队善于掌握哪种统一语言,如果都熟悉服务,那么SOA也许合适,但是它也有缺陷,忽视领域模型的地位了。
如果熟悉事件,当然ES和CQRS是选择,而且事件是围绕DDD实体的,这和DDD比较接近。
如果熟悉场景角色,DCI是一种选择,这也是围绕DDD实体的。
Jdon框架推崇DDD+DCI+ES/CQRS。
JdonFramework = DDD + DCI + ES/CQRS
因为DCI和ES虽然正交,但是不矛盾,DDD的实体充当DCI的数据模型,DCI的Context是角色扮演场所,角色实施的行为是事件,事件由角色在一定场景下发出。
Jdon框架默认编程统一语言是:
由领域模型(DDD)扮演的角色(Role of DCI)在用例场景(Context of DCI)下触发了一个交互事件(interaction event)。
为什么需要事件?
以货运为案例,如下图:
有些人会误解ShoppingEvent为实体,事件如果是一个实体,变成事件的记录了(事件变成了状态),并不能代表正在发生的事件。
而我们根据前面统一语言,设计如下:
public Class Ship{
@Inject
ShippingRole shippingRole;
.....
}
Ship是data model也就是领域模型 ,ShippingRole是角色,由角色ShippingRole发出各种装船事件 比如到达 卸货等事件。
有些DCI实现是将Ship注入到ShippingRole中,我个人认为对阅读代码理解不利,你要了解领域模型的所有功能,必须一个个找它被注入到了那些角色场景中,这和SOA下被服务类操作使用有什么区别呢?
将领域模型的所有可能扮演的角色陈列在领域模型的,而角色的行为都分离到角色中,这样有助于我们只要打开领域模型类的代码,大概知道这个模型实现哪些功能?
以上是从业务角色看实现的ShippingRole角色,那么从技术架构角度看,领域模型还有持久化自己等等,按照职责驱动开发方法,无论是业务要求或技术要求,这些都是领域模型应该干的事情,应该承担的职责,难道业务上干的事情在领域模型中,技术要要求干的就不让领域模型干了?
由此,我们从对象职责这个角度综合业务和技术要求,得出领域模型如下:
public Class Ship{
@Inject
LazyOperatorRole lazyOperatorRole;
@Inject
ShippingRole shippingRole;
.....
}
其中LazyOperatorRole角色负责对实体Ship的其他次要字段根据需要进行加载,或者根据需要进行数据库持久保存。
其实,这里LazyOperatorRole和ShippingRole也是DDD中强调的Bounded Context,因为谈到Context,总是相伴角色的,比如DCI中谈到Context,隐式地其实围绕角色Role。
我们在实体中通过引入符合一定BoundedContext的角色,将这些BoundedContext下发生的行为分离进入角色,由角色通过发送事件给外界,如果不使用事件发送,这些角色行为实现不管是业务行为还是技术行为,总是不可预期的,它们可能是技术和业务混合在一起调用,比如查个数据库,发邮件,关联一下字段等等,这些复杂行为反而会弄脏领域模型,而只有通过事件引入,才能分离领域模型和其生存的环境。
所以,事件的引入已经不只是对业务有好处(业务本身就有事件如shipping事件),而且对架构有好处(如存储事件),更主要保证领域模型不会被太多细节包括技术细节覆盖,突出显示其作为系统核心,其代表用户需求的中心地位。
面向DDD的事件驱动架构
为了更好地突出应用需求为主,Jdon框架采取面向DDD的主要设计思想,主要特点是常驻内存in-memory的领域模型向技术架构发送事件消息,驱动技术架构为之服务,如同人体的DNS是人体各种活动的主要控制者和最高司令,领域模型是一个软件系统的最高司令。
JF有五种模型组件,如下:
一. 实体聚合根对象 元注释 @Model;
二. 服务Service 元注释 @Service;
三. 普通类组件构件 @Component;
四. 生产者Prodcuer-消费者模型 @send @Consumer;
五. 拦截器 @ Interceptor, 首先需要导入点 @Introduce;
所有都在 com.jdon.annotation.*包中。
这些模型组件其实有划分为两大类:业务和技术:
常驻内存@Model 领域模型,包括实体模型 值对象和领域服务,与技术架构无关。相当于鱼;生存空间是缓冲器中
@Component 技术组件架构,用以支撑领域模型在计算机系统运行的支撑环境,相当于鱼生活的水。空间在Context container,例如ServletContext中。
两者以Domain Events模式交互方式: 领域模型向技术组件发出异步命令。
实体模型
根据DDD方法,需求模型分为实体模型 值对象和领域服务三种,实际需求经常被划分为不同的对象群,如Cargo对象群就是Cargo为根对象,后面聚合了一批与其联系非常紧密的子对象如值对象等,例如轿车为根对象,而马达则是轿车这个对象群中的一个实体子对象。
在Jdon框架中,实体根模型通常以@Model标识,并且要求有一个唯一的标识主键,你可以看成和数据表的主键类似,它是用来标识这个实体模型为唯一的标志,也可以使用Java对象的HashCode来标识。
Jdon框架是实体模型的主键标识有两个用处:
首先是用来缓存实体模型,目前缓冲器是使用EHcache,可以无缝整合分布式云缓存Terracotta来进行伸缩性设计。
只要标识@Model的实体,在表现层Jsp页面再次使用时,将自动直接从缓存中获得,因为在中间业务层和表现层之间Jdon框架存在一个缓存拦截器CacheInterceptor,见框架的aspect.xml中配置。
为了能够在业务层能够使用缓存中的模型,需要在业务层后面的持久层或Repository中手工加入缓存的Annotation标签,如下:
实体模型的缓存架构如下:
注意:这里可能是Jdon框架 6.2的一个创新或重要点,在JF6.2之前实际上没有对模型进行突出的支持,就象画个圈,圈子外面基本都就绪,圈子里面留白,这个圈子就是Domain Model,原来因为考虑持久层Hibernate等ORM巨大影响力,就连Spring也是将Model委托给Hibernate处理,自己不做模型层,但是当NoSQL运动蓬勃发展,DDD深入理解,6.2则找到一个方式可以介入模型层,同时又不影响任一持久层框架的接入。
Jdon框架6.2通过在持久层的获得模型对象方法上加注释的办法,既将模型引入了内存缓存,又实现了向这个模型注射必要的领域事件Domain Events。
事件Event Sourcing
JF的最大特点是领域模型驱动技术架构;
如果说普通编程缺省是顺序运行,那么事件模型缺省是并行运行,两者有基本思路的不同。
Event Sourcing适合将复杂业务领域和复杂技术架构实现分离的不二之选。实现业务和技术的松耦合,业务逻辑能够与技术架构解耦,将”做什么”和”怎么做”分离
事件模型也是一种EDA架构Event-driven Architecture,可以实现异步懒惰加载Asynchronous Lazy-load类似函数式语言的懒功能,只有使用时才真正执行。
具有良好的可伸缩性和可扩展性,通过与JMS等消息系统结合,可以在多核或多个服务器之间弹性扩展。
事件模型也是适合DDD落地的最佳解决方案之一。领域模型是富充血模型,类似人类的DNA,是各种重要事件导向的开关。用户触发事件,事件直接激活领域模型的方法函数,再通过异步事件驱动技术活动,如存储数据库或校验信用卡有效性等。
2009年Jdon框架6.2就推出了Domain Model + In-memory + Events.,2001年Martin fowler在其文章LMAX架构 推荐In-memory + Event Sourcing架构。以下是该文的精选摘要,从一个方面详细说明了事件模型的必要性:
内存中的领域模型处理业务逻辑,产生输出事件,整个操作都是在内存中,没有数据库或其他持久存储。将所有数据驻留在内存中有两个重要好处:首先是快,没有IO,也没有事务,其次是简化编程,没有对象/关系数据库的映射,所有代码都是使用Java对象模型。
使用基于内存的模型有一个重要问题:万一崩溃怎么办?电源掉电也是可能发生的,“事件”(Event Sourcing )概念是问题解决的核心,业务逻辑处理器的状态是由输入事件驱动的,只要这些输入事件被持久化保存起来,你就总是能够在崩溃情况下,根据事件重演重新获得当前状态。
事件方式是有价值的因为它允许处理器可以完全在内存中运行,但它有另一种用于诊断相当大的优势:如果出现一些意想不到的行为,事件副本们能够让他们在开发环境重放生产环境的事件,这就容易使他们能够研究和发现出在生产环境到底发生了什么事。
这种诊断能力延伸到业务诊断。有一些企业的任务,如在风险管理,需要大量的计算,但是不处理订单。一个例子是根据其目前的交易头寸的风险状况排名前20位客户名单,他们就可以切分到复制好的领域模型中进行计算,而不是在生产环境中正在运行的领域模型,不同性质的领域模型保存在不同机器的内存中,彼此不影响。
LMAX团队同时还推出了开源并发框架Disruptor,他们通过测试发现通常的并发模型如Actor模型是有瓶颈的,所以他们采取Disruptor这样无锁框架,采取了配合多核CPU高速缓冲策略,而其他策略包括JDK一些带锁都是有性能陷阱的: JVM伪共享。
JF的领域事件是基于号称最快的并发框架Disruptor开发的,因此JF的事件是并行并发模型,不同于普通编程是同一个线程内的顺序执行模型。
JF的领域事件是一种异步模式 + 生产者-消费者模式。主题topic和Queue队列两种。领域模型是生产者;消费者有两种:
.@Consumer;可以实现1:N多个,内部机制使用号称最快的并发框架Disruptor实现。适合轻量;小任务;原子性;无状态。
.@Componet;直接使用普通组件类作为消费者,使用jdk future机制,只能1:1,适合大而繁重的任务,有状态,单例。
Domain Events实现机制如下:
JF的事件模型还是一种CQRS架构,可以实现模型的修改和查询分离,也就是读写分离的架构:
无堵塞的并发编程
顺序编程和并发编程是两种完全不同的编程思路,堵塞Block是顺序编程的家常便饭,常常隐含在顺序过程式编程中难以发现,最后,成为杀死系统的罪魁祸首;但是在并发编程中,堵塞却成为一个目标非常暴露的敌人,堵塞成为并发不可调和绝对一号公敌。
因为无堵塞所以快,这已经成为并发的一个基本特征。
过去我们都习惯了在一个线程下的顺序编程,比如,我们写一个Jsp(PHP或ASP)实际都是在一个线程
下运行,以google的adsense.Jsp为例子:
<%
//1.获得当前时间
long googleDt = System.currentTimeMillis();
//2.创建一个字符串变量
StringBuilder googleAdUrlStr = new StringBuilder(PAGEAD);
//3.将当前时间加入到字符串中
googleAdUrlStr.append("&dt=").append(googleDt);
//4.以字符串形成一个URL
URL googleAdUrl = new URL(googleAdUrlStr.toString());
%>
以上JSP中4步语句实际是在靠一个线程依次顺序执行的,如果这四步中有一步执行得比较慢,也就是我们所称的是堵塞,那么无疑会影响四步的最后执行时间,这就象乌龟和兔子过独木桥,整体效能将被跑得慢的乌龟降低了。
过去由于是一个CPU处理指令,使得顺序编程成为一种被迫的自然方式,以至于我们已经习惯了顺序运行的思维;但是如今是双核或多核时代,我们为什么不能让两个CPU或多个CPU同时做事呢?
如果两个CPU同时运行上面代码会有什么结果?首先,我们要考虑两个CPU是否能够同时运行这段逻辑呢?
考虑到第三步是依赖于前面两步,而第二步是不依赖第一步的,因此,第一步和第二步可以交给两个CPU同时去执行,然后在第三步这里堵塞等待,将前面两步运行的结果在此组装。
很显然,这四步中由于第三步的堵塞等待,使得两个CPU并行计算汇聚到这一步又变成了瓶颈,从而并不能充分完全发挥两个CPU并行计算的性能。
我们把这段JSP的第三步代码堵塞等待看成是因为业务功能必须要求的顺序编程,无论前面你们如何分开跑得快,到这里就必须合拢一个个过独木桥了。
但是,在实际技术架构中,我们经常也会因为非业务原因设置人为设置各种堵塞等待,这样的堵塞就成为并行的敌人了,比如
我们经常有(特别是Socket读取)
While(true){
……
}
这样的死循环,无疑这种无限循环是一种堵塞,非常耗费CPU,它也无疑成为并行的敌人。
比如JDK中java.concurrent. BlockingQueue LinkedBlockingQueue,也是一种堵塞式的所谓并行包,这些并行功能必须有堵塞存在的前提下才能完成并行运行,很显然是一种伪并行。
由于各种技术上的堵塞存在,包括多线程中锁机制,也是一种堵塞,因为锁机制在某个时刻只允许一个线程进行修改操作,所以,并发框架Disruptor可以自豪地说:无锁,所以很快。
现在非常流行事件编程模型,如Event Sourcing或Domain Events或Actor等等,事件编程实际是一种无堵塞的并行编程,因为事件这个词语本身有业务模型上概念,也有技术平台上的一个规范,谈到事件,领域专家明白如同电话铃事件发生就要接,程序员也能明白只要有事件,CPU就要立即处理(特别是紧急事件),而且事件发生在业务上可能是随机的,因此事件之间难以形成互相依赖,这就不会强迫技术上发生前面Jsp页面的第三步堵塞等待。
因此,在事件模型下,缺省编程思维习惯是并发并行的,如果说过去我们缺省的是进行一个线程内的顺序编程,那么现在我们是多线程无锁无堵塞的并发编程,这种习惯的改变本身也是一种思维方式的改变。
在缺省大多数是并发编程的情况下,我们能将业务上需要的顺序执行作为一种特例认真小心对待,不要让其象癌细胞一样扩散。我们发现这种业务上的顺序通常表现为一种高一致性追求,更严格的一种事务性,每一步依赖上一步,下一步失败,必须将上一步回滚,这种方式是多核CPU克星,也是分布式并行计算的死穴。值得庆幸的是这种高一致性的顺序编程在大部分系统中只占据很小一部分,下图是电子商务EBay将他们的高一致性局限在小部分示意图:
由此可见,过去我们实现的顺序编程,实际上是我们把一种很小众的编程方式进行大规模推广,甚至作为缺省的编程模式,结果导致CPU闲置,吞吐量上不去同时,CPU负载也上不去,CPU出工不出力,如同过去计划经济时代的人员生产效率。
据统计,在一个堵塞或有锁的系统中, 等待线程将会闲置,这会消耗很多系统资源。消耗资源的公式如下:
闲置的线程数 =(arrival_rate * processing_time)
如果arrival_rate(访问量达)很高,闲置线程数将会很大。堵塞系统是在一个没有效率的方式下运行,无法通过提高CPU负载获得高吞吐量。
避免锁或同步锁有多种方式,volatile是一种方式,主要的不变性设计,比如设计成DDD的值对象那种,要变就整个更换,就不存在对值对象内部共享操作;还有克隆原型,比如模型对象克隆自己分别传给并发的多个事件处理;用Visibility 使资料对所有线程可见等等;
最彻底的方式就是使用无锁Queue队列的事件或消息模型,Disruptor采取的是一个类似左轮手qiang圆环RingBuffer设计,见http://www.jdon.com/jivejdon/thread/42466,这样既能在两个线程之间共享状态,又实现了无锁,比较巧妙,业界引起震动。
当然Scala的那种Share nothing的Actor模型也是一种无锁并发模型,不过奇怪的是,发明Disruptor的LMAX团队首先使用过Actor模型,但是并发测试发现还是有性能瓶颈的,所以,他们才搞了一个Disruptor。
领域模型发出事件,事件由Disruptor的RingBuffer传递给另外一个线程(事件消费者);实现两个线程之间数据传递和共享。
当事件消费者线程处理完毕,将结果放入另外一个RingBuffer,供原始线程(事件生产者)读取或堵塞读取,是否堵塞取决于是否需要顺序了。
非堵塞并发使用模式如下:
发出调用以后不必立即使用结果,直至以后需要时才使用它。发出调用后不要堵塞在原地等待处理结果,而是去做其他事情,直至你需要使用这个处理结果时才去获取结果。
被调用者将会返回一个“future”句柄,调用者能够获得这个句柄后做其他事情,其他事情处理完毕再从这个句柄中获得被调用者的处理结果。
JF 6.5版本为推广适合多核CPU的无堵塞并发编程范式进行了探索,使用了Domain Events和DCI等不同抽象层次对并发编程进行了封装,从而降低开发者使用并发编程的难度。
DCI架构
DCI:数据Data, 上下文(场景)Context, 交互Interactions是由MVC发明者Trygve Reenskaug发明的。其核心思想是:
让我们的核心模型更加简单,只有数据和基本行为。业务逻辑等交互行为在角色模型中在运行时的场景,将角色的交互行为注射到数据中。
Jdon框架提供了两种DCI风格实现:一种是将DomainEvents对象注射进入当前模型;还有一种是直接将数据模型和接口混合。这两种方式适合不同场景。
如果我们已经Hold住了一个领域对象,那么就直接通过其发出领域事件实现功能;比如模型的修改。在这种模式下,发出领域事件的领域模型本身已经隐含了场景。事件代表场景出头牵线。
否则,我们显式创建一个上下文Context, 在其中通过RoleAssigner将角色接口注入到领域对象中。比如模型新增创建或删除。(对象不能自己创建自己)。
具体实现可见下面专门DCI案例章节。
依赖注入DI
(一)@Model:模型中可以通过字段的@Inject将其他类注射进入,包括@Component类。被注射的类如果有@Introduce,将再次引入拦截器。
(二)@Component:技术架构中组件可以通过构造器直接注射,被注射的类如果有@Introduce,将再次引入拦截器。
相关推荐
【标题】:“BANQ-u盘量产工具” 【描述】:在日常使用中,U盘可能会遇到各种问题,如无法格式化或无法正常工作。这时,我们可以通过使用“量产工具”来解决这些问题,让U盘恢复到初始状态,即所谓的“量产”。量产...
关于DDD可参考InfoQ的Mini Book Domain Driven Design Quickly , 还有 Banq 的文章 实战DDD(Domain-Driven Design领域驱动设计), 和 领域模型驱动设计(DDD)之模型提炼 。 本书分为四部分,第一部分讲述了领域模型...
【BaNQ-Clipper: 自动将您的BAnQ贷款与Google日历同步】 BaNQ-Clipper是一款基于JavaScript开发的应用程序,其主要功能是帮助用户将BAnQ的贷款还款信息自动同步到Google日历中。通过这种方式,用户可以更方便地管理...
【标题】"量产U盘修复工具"涉及到的关键知识点主要包括U盘修复技术和量产工具的使用。 在USB闪存盘(简称U盘)的使用过程中,由于各种原因,如病毒攻击、误操作或硬件故障,可能会导致U盘无法正常读写,这时就需要...
BanQ:一种以视频解释和代码形式同时众包算法解决方案的应用程序。 目录 建于 入门 快来了! 安装 分叉仓库 克隆仓库 git clone https://github.com/your_username_/BanQ.git 安装NPM软件包 npm install npm开始 ...
标题中的“U盘修复神器”指的是专门用于解决U盘故障的软件工具,这类工具通常能够帮助用户修复无法识别或在磁盘管理中未显示的U盘问题。在描述中提到的情况,用户表示自己的U盘在电脑上无法正常识别,甚至在磁盘管理...
标题中的“优盘usb写保护开关”指的是USB闪存盘上的一个物理功能,它允许用户启用或禁用设备的写保护状态。写保护开关的主要目的是防止未经授权的数据修改或删除,同时也能有效地避免计算机病毒通过自动运行功能在...
标题中的“U盘无法格式化解决方法[录像教程]”是指一种针对计算机用户遇到的常见问题,即USB闪存驱动器(通常称为U盘)在尝试格式化时遇到障碍的解决方案。这种问题可能由多种原因引起,包括病毒感染、文件系统错误...
U盘是由主控板+FLASH+外壳组成的,当主控板焊接上空白FLASH后插入电脑,因为没有相应的数据, 电脑只能识别到主控板,而无法识别到FLASH,所以这时候电脑上显示出U盘盘符,但是双击盘符却显示没有插入U盘,就像是...
【U盘量产工具中文版】是一款专为解决MP3播放器、U盘等移动存储设备在使用过程中出现的无法读取、格式化错误等问题而设计的实用软件。它通过模拟USB设备制造商的生产过程,对U盘进行初始化、分区、格式化等一系列...
"U盘量产工具"是指用于批量生产和修复USB闪存盘的专用软件,它主要用于调整U盘的参数设置、格式化、修复故障以及优化性能。在本案例中,我们讨论的是MW8229_59_69UTools_V10.4_1207这个版本的工具。...
SD卡格式化工具TF卡格式化工具U盘格式化单文件绿色版,直接使用无需安装
JdonFramework need above jdk 1.4.0 This version has passed under Tomcat 4.x/5.x JBoss 3.x/JBoss 4.0.0 Weblogic 8.1 when build this project with eclipse or jbuilder. you need modify build.xml , ...banq
标题中的“TF工具_U盘修复工具”指的是针对TF(TransFlash)卡或常见的USB闪存盘(U盘)的一种修复程序。这类工具旨在解决U盘出现的一些常见问题,如文件系统损坏、无法读取、容量显示异常等。修复工具通常会尝试对...
文章作者是banq(又称为板桥里人),他是一位知名的软件架构师和设计模式专家。通过这份分析报告,读者可以学习到如何在实际项目中合理运用设计模式,以及理解优秀软件系统背后的架构设计理念。 #### Jive系统概述 ...
设备名称: [J:][G:]USB Mass Storage Device(磁盘驱动器)(Kingmax USB2.0 FlashDisk USB Device) PNP设备ID: VID = 1687 PID = 0165 设备序列号: 090402756A039B 设备版本: 0.00/0.00 设备类型: 标准USB设备 - ...
该框架由板桥里人(banq)创建并维护,自2005年7月14日发布1.2.2版本以来,已经广泛应用于多个项目中。Jdon框架的官方网站为[http://www.jdon.com](http://www.jdon.com),用户可以通过此网站获取技术支持和参与社区...
标题中的“U盘工具 量产助手”指的是用于批量生产和格式化U盘的软件工具,它可以帮助用户对多个U盘进行相同的操作,如设置启动盘、修复故障或统一格式化。在IT领域,"量产"一词通常指的是用特定工具一次性处理大量...
通过‘量产’这个过程,我们可以对SD卡进行深层次的管理和修复。” SD卡量产工具,顾名思义,是用于批量生产或定制SD卡的软件,它不仅能完成基本的格式化操作,还能调整卡的参数,如容量、速度等级、分区类型等,...