DCI,领域模型,领域事件的一些想法

10-03-25 xmuzyu
                   

去年我们讨论了很多关于异步,伸缩性架构的主题。近期我们也讨论DCI的一些主题。我就DCI,DDD以及领域事件说说自己的想法。

领域模型和领域事件本站已经讨论了很多,本人就略过,下面我结合本人的亲身经历说说关于DCI架构的一些想法。

首先,我们想一个问题,当前的JAVA语言是否真正的容易实现充血模型。充血模型讲究领域模型对象具有丰富的职责和行为,但是因为目前java语言不支持Mixin特性,这样就会造成一个实体对象的属性,行为非常的多,并且如果随着后来系统的进一步迭代,你会发现领域实体对象的属性以及行为很多,而这些行为放入domain service又觉得不恰当,放到实体中,又发现实体已经有了非常多的属性方法,这样如果属性和方法很多,维护成本显然变高,这样其实就是没有做好解耦和封装。因此单纯的依赖java语言以及纯业务的对象建模,实现一个真正行为丰富的充血模型感觉会有点别扭。

既然上面说有点别扭,那么有没有什么补救的方法呢?有,这就是DCI。其实早在数年前,彩色UML的思想就很类似于DCI,只不过08年以后,随着彩色UML建模这本中译本在大陆市场的发行,大家才慢慢接触到四色原型的概念。不过如果经常上jdon的人,应该对彩色UML是非常熟悉。彩色UML主要是说:某个人,事,地点在某个时间段内,以某种角色,发生了什么样子的行为。而这里面和DCI架构其实非常的神似。DCI架构中有场景,数据和交互,而这里场景我觉得就是一种职责和属性的载体。当我们再回到刚才的充血模型的话题,我刚才说了如果往一个实体对象塞入太多的属性和方法,势必也会造成实体对象变的,维护成本变高,那么这个时候,我们就需要对其进行切分而这种切分的标准其实就是按照DCI来进行,实体以某一种角色参与到某一种场景中来,而这种行为同时也会涉及到一些属性,而这里的属性就是我们所说的场景属性,这个时候,场景对象我们要保证它的不变了约束,这里可以引入场景工厂模式来对场景的不变量约束进行保证。

好了,上面说了自己对DCI架构的一些理解,下面我就以一个实际的例子来说一下如何具体来实施我们的这些想法。这个列子也来自以前的一个项目,现在我结合DCI架构对其进行一个小的重构工作。首先我来描述一下,项目的场景。

在系统中存在某一个实体,我们暂时命名为Story(文章或者故事),我们的系统不仅需要记录某个Story被顶了多少次,而且还要记录哪些人顶了某个Story,我先说说,我以前具体的做法:

首先我利用了模型利用领域事件驱动技术架构的方式,具体事件如何处理,我采用了Vistor模式,通过Vistor模式来实现事件处理,但是Vistor模式要求Visteable的类继承体系相对稳定,而我们系统中的领域事件是随着系统的迭代扩展而不断增加进来的,因此这个地方仅仅利用访问者模式还不行,于是我结合回调模式,结合Vistor模式实现了可以动态扩展的处理各种事件的方式。具体的代码如下:

 //DomainEvent是领域事件基类,系统中的其它领域事件都需要继承此类,同时采用Vistor模式
 public class DomainEvent implements Vistable{
 
     public void accept(EventHandler eventHandler){
        eventHandler.vistor(eventCallback());
     } 
   
   //此方法是钩子方法,模板方法,子类,比如顶贴的事件,增加好友的事件等,都要实现此方法
    protected abstract EventCallback eventCallback(); 

   ......其它的方法略过
}

//DiggStoryEvent表示顶贴的事件,每次用户顶贴的是很,领域模型对象会触发此事件来完成后续的逻辑操作
public class DiggStoryEvent extends DomainEvent{
    //所有的事件类,只需要继承DomainEvent,然后override eventCallback()方法即可。
    public EventCallback eventCallback(){
    
    //这里使用匿名内部类返回具体的事件类型	 
    return new EventCallback(){
        
		  public DomainEvent getDomainEvent(){
			  new EventCallback(){
				  return DiggStoryEvent.this;
			  };
		  }
		 };
	}
}

//事件Handler,有一点点类似一个ESB消息总线一样,它负责接收事件,然后转发给不同的事件处理器
public class DefaultEventHandler implements EventHanlder{
   
   //事件处理器注册器,每一个事件都可以注册三种类型的事件处理器,异步,同步,nullEventProcessor
   private EventProcessorRegister eventProcessorRegister;
		
   public void vistor(EventCallback eventCallback){
      DomainEvent domainEvent = eventCallback.getDomainEvent();
      
      //事件处理器注册器根据具体的DomainEvent查找到对应的事件处理器
       EventProcessor eventProcessor = eventProcessorRegister.lookup(domainEvent);
      
      /**这里面的事件处理器,一共有三种策略,同步事件处理,异步事件处理,还有NullEventProcessor,
       *空事件处理器主要用来在系统运行的时候暂停某个功能,比如假设系统想停止顶贴这个功能,那么
        *可以动态的修改DiggStoryEvent对应的事件处理器为NullEventProcessor
       */
     eventProcessor.processEvent(domainEvent); 
   }  
       
<p>

好了,以上是系统事件处理的几个主要类。今天晚上就先写到这里,明天下班回来接着写,欢迎各位道友讨论。

                   

18
banq
2010-03-26 10:06

不错设计。

关键是Story如何与你的事件组件DiggStoryEvent如何交互,是把DiggStoryEvent注射到Story中,还是由DiggStoryEvent驱动Story,以javaEE 6的CDI设计为例:

@Inject
private javax.enterprise.event.Event<User> userEvent;
//两者交互关键在这一句,由userEvent驱动user,user作为方法参数传入
//这是事件组件驱动领域模型,好的设计应该倒过来。
userEvent.fire(user);
<p>

xmuzyu
2010-03-26 21:46

呵呵,今天接着写。昨天晚上只写了一部分于时间处理有关系的类,下面我们来看看重构之前和重构之后,以及整个重构过程的思考。

首先我们来看一下没有采用场景的方式,以下是DiggStorySynchronizedEventProcessor处理器的实现方式,它是一个同步的事件处理器,异步的类似。

// 以下是DiggStorySynchronizedEventProcessor的实现:
 public class DiggStorySynchronizedEventProcessor implements DomainEventProcessor{
		
		private DiggStoryEventRepository diggStoryEventRepository;
		
		private FeedRepository feedRepository;
		
		public void processEvent(DomainEvent domainEvent){
			DiggStoryEvent diggStoryEvent = (DiggStoryEvent)domainEvent;
			
			//存储DiggStoryEvent以便以后统计哪些人顶了Story
			this.diggStoryEventRepository.add(diggStoryEvent);
			
			//新建一个Feed,表示某个人顶了某个Story,
			Feed feed = new Feed();
			Property property = new Property();
			property.setName("title");
			property.setValue("Digg the story"+diggStoryEvent.getStory().getName());
			
			//....动态的增加新的Property
			feed.add(property);
			
			//保存Feed,以便让好友可以看到自己的动态
			this.feedRepository.add(feed);

                       //以后也许会有新的行为加入
		}	
	
  }

  

<p>

好了,有了DiggStorySynchronizedEventProcessor ,我们再看看Story如何与其交互,以下是Story的实现:

// Story实现:
public class Story implements Diggable {
   
    public void digg(EventHandler eventHandler,User user){

	DiggStoryEvent diggStoryEvent = new DiggStoryEvent(this,user);
	diggStoryEvent.accept(eventHandler);
	
    }

		
}

<p>

好了,以上就是没有重构之前的代码,我们分析一下这样做会带来什么不好的后果,我们知道事件处理器本身是技术的组件,以后是可以替换的,而这里我们在事件处理器里写了很多的代码,也许以后还有进行其它的行为的拓展(这包括保存事件本身,保存Feed等等),这样以来,表面上好像是领域模型+领域事件了,好像是面向对象了,其实还不够彻底。技术组件不应该实现逻辑的,业务逻辑应该由模型来实现,技术组件仅仅是调用模型的逻辑而已,那么这里的逻辑到底放到什么模型里面呢?如果没有引入场景的情况下,我们也许会想到领域服务,但是如果真这样,我们又会发现以后,领域服务的代码将变的不好维护,因此这个时候就是需要引入场景的地方了,因为具体的场景对象具有当前顶这个实际场景的语义,这也是与场景所符合的。

OK,知道场景的概念以后,我们又会遇到一个问题,那么就是场景对象的生命周期,因为万事万物都有生命周期,场景也一样,它也得生,也得死,因此,我们就来考虑一下场景的生命周期,起初重构的时候,我引入了EventContext的概念,一个事件发生的时候对应相应的事件上下文,这样我就将上面的DiggStorySynchronizedEventProcessor的代码移到了EventContext里实现,这样实现是实现了,但是我后来发现,EventContext这个纯虚构(GRASP模式之一)的模型是否真正的合适,因为采用EventContext实现以后,我发现上下文对象的生命周期和Event的生命周期是不一致的,因为有时候场景的行为也会触发事件,场景和事件是不能强耦合的,同时因为事件需要在多个分布式节点之间传递,因此我们需要保证事件的“容量”,它不能太大,这里的太大包括属性和行为等。因此我后来废掉了EventContext,引入了DiggStoryContext对象,DiggStoryContext对象负责在顶贴这个行为发生的时候,完成相应的逻辑处理,同时事件和场景还有个先后顺序的关系,首先场景是由用户触发的,而事件是由模型触发的,用户首先顶一帖子,业务层会创建一个DiggStoryContext对象,让后Story以被顶者的角色加入到当前的场景,当Story加入到顶贴这个场景中之后,Story触发DiggStoryEvent事件。

呵呵,经过上面的分析,我进一步进行了重构,最终重构后的代码如下:

首先来看看DiggStoryContext:

// DiggStoryContext实现:
/**
  *Context是场景的基类,以后所有的场景都需要继承Context,并实现doContext()方法
  */
public class DiggStoryContext extends Context{
          
	private Story story ;
		
	//顶贴的人
	private User user;
		
	private FeedRepository feedRepository;
		
		
	@override
	public void doContext(){
		//新建一个Feed,表示某个人顶了某个Story,
		Feed feed = new Feed();
		Property property = new Property();
		property.setName("title");
		property.setValue("Digg the story"+diggStoryEvent.getStory().getName());
		
		//....动态的增加新的Property
		feed.add(property);
		
		//保存Feed,以便让好友可以看到自己的动态
		this.feedRepository.add(feed);
                
                //场景的行为以后还可能扩展
	}

       //利用工厂方法创建场景,保证场景的不变量,关于为什么这样做,下文会描述
       public static DiggStoryContext create(Story story,User user,FeedRepository feedRepository){
             DiggStoryContext diggStoryContext = new DiggStoryContext(story,user,feedRepository);
             return diggStoryContext;    

       }

}

//以下是重构以后的DiggStorySynchronizedEventProcessor 
 public class DiggStorySynchronizedEventProcessor implements DomainEventProcessor{
			
	private DiggStoryEventRepository diggStoryEventRepository;
		
	private FeedRepository feedRepository;
	
        //这个时候,事件处理器不会涉及到具体的业务操作,它仅仅通过调用模型对象的方法即可		
	public void processEvent(DomainEvent domainEvent){
		DiggStoryEvent diggStoryEvent = (DiggStoryEvent)domainEvent;
			
		//存储DiggStoryEvent以便以后统计哪些人顶了Story,同时也可以进行事件回放
		this.diggStoryEventRepository.add(diggStoryEvent);
				
		domainEvent.execute();
							
	}
	
   }

//以下是DiggStoryEvent的实现:
public class DiggStoryEvent extends DomainEvent{
       
        //这里需要注意,不是每一个领域事件都有场景的
	private DiggStoryContext diggStoryContext;
  
	
	public void execute(){
	  this.diggStoryContext.doContext():
	}
	
	//其它的代码和以前类似

         
}



<p>

好了,最后,我们再来看看Story重构之后如何于其它的模型交互。以下是Story重构后的结构:

public class Story implements Diggable {
	
		
	public void digg(EventHandler eventHandler,DiggStoryContext diggStoryContext){
		DiggStoryEvent diggStoryEvent = new DiggStoryEvent(this,user);
                diggStoryEvent.setDiggContext(diggStoryContext)
		diggStoryEvent.accept(eventHandler);
	}
}
<p>

好了,经过重构,我们发现事件处理器的代码很简单,几乎没有任何逻辑,所有的逻辑其实都是由场景对象来实现,但是这里面我们还需要考虑一个问起,那就是场景对象的不变量约束,因为场景对象有行为和属性,行为表示当前系统发生的交互,而属性就是当前交互所设计的系统其它对象,所以此时场景对象的不变量约束很重要,而不变量约束我们一般都是通过工厂模式来解决,因此此时我们引入工厂方法来完成DiggStoryContext的创建。

OK了,重构过程结束,最后在总结以下整个系统流程。当用户进行操作的时候,首先创建场景对象,然后领域模型对象以某种角色加入到场景中来,让后场景完成具体的逻辑操作,业务操作完成,场景生命周期结束。如果领域模型还会触发事件的话,那么事件处理器会进行相应的事件处理,而这里事件处理里的实现逻辑也需要有场景对象来完成。因此模型应该实现逻辑,模型驱动技术组件完成逻辑操作。打个比方,领域模型好比我们自己,而架构就好比社会,架构为模型提供了一个大的环境,但是在这个社会中,每个人能做什么事情,还得我们自己(模型)来完成,社会是不会替我们完成什么任务的,我们的自己来。

说到这里,我们发现四色原型和DCI有很大的相似之处,四色原型中的红色MI模型应该就是当前的场景,而黄色的role模型就是模型的角色,而绿色的PPT模型就是领域实体模型.

[该贴被xmuzyu于2010-03-26 22:04修改过]

banq
2010-03-27 10:40

2010年03月26日 21:46 "xmuzyu"的内容
首先场景是由用户触发的,而事件是由模型触发的,用户首先顶一帖子,业务层会创建一个DiggStoryContext对象,让后Story以被顶者的角色加入到当前的场景,当Story加入到顶贴这个场景中之后,Story触发DiggStoryEve ...

这个思路很好,如何处理场景和事件之间关系也是我一直在考虑的。

场景是由角色参与的,这里是顶者和被顶者两个角色。而事件起源应该来自模型,事件模式分触发者生产者和消费者响应处理者,如何把这两者在场景中落实,有不同的思考角度。

事件起源来自模型,模型应该是事件的生产者,那么场景应该是事件的消费者响应者。如果我们从事件模式生产和消费角度来设计代码,这是一个通用的方式,CDI中events也是这种方式(fire表示生产,observer表示消费观察者),以模式概念来交流起来更方便些。xmuzyu辛苦了,如果再按这个方式重构一下就更完善了。

我又想到另外一个方向问题,前面我们讨论场景和事件融合问题,现在跳出这个框框,看场景是否需要和事件融合,或者说,场景和事件本身就是重叠概念,可能都能代表对方,只是不同的表达方式而已。

xmuzyu
2010-03-27 19:20

2010年03月27日 10:40 "banq"的内容
这个思路很好,如何处理场景和事件之间关系也是我一直在考虑的。

场景是由角色参与的,这里是顶者和被顶者两个角色。而事件起源应该来自模型,事件模式分触发者生产者和消费者响应处理者,如何把这两者在场景中落实,有不同的思考角度。

事件起 ...

关于场景和事件,我之前重构的时候也考虑了一下。我觉得事件的触发点有两个:

第一个就是模型自己触发事件,也就是模型不加入任何场景,属于模型本身的行为,不属于场景行为,此时事件触发是由模型本身的行为触发的。

第二个就是模型以一种角色加入到某种场景以后,场景行为也会触发事件,而这种事件此时相当于是场景行为触发的事件。

无论是模型本身触发的事件还是场景行为触发的事件,这两个的事件处理应该是一致的,需要统一管理。但是具体事件如何处理,也应该由模型来实现,事件处理器仅仅是一种技术方面的组件,处理器仅仅是调度者,它负责调度模型来完成事件的响应。同步,异步的处理器仅仅是提供了一种技术支持环境,但是具体的逻辑如何实现,还是要由模型实现,不过执行的时候,可以放到同步或者异步的环境中执行。

4Go 1 2 3 4 下一页