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

去年我们讨论了很多关于异步,伸缩性架构的主题。近期我们也讨论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);
}

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

不错设计。

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


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

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

首先我们来看一下没有采用场景的方式,以下是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);

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

}



好了,有了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);

}


}

好了,以上就是没有重构之前的代码,我们分析一下这样做会带来什么不好的后果,我们知道事件处理器本身是技术的组件,以后是可以替换的,而这里我们在事件处理器里写了很多的代码,也许以后还有进行其它的行为的拓展(这包括保存事件本身,保存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():
}

//其它的代码和以前类似


}



好了,最后,我们再来看看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);
}
}

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

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

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



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

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

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

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

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

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

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

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

事件起 ...

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

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

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

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

2010年03月27日 10:40 "banq"的内容
场景和事件本身就是重叠概念,可能都能代表对方,只是不同的表达方式而已 ...

我从这个角度深入思考了一下:

回到问题源头:为什么有贫血失血模型?因为将业务方法都放入Web服务等业务服务中了,这些业务方法本可能属于实体,这种抽取行为造成了实体对象的贫血。这是SOA组件构件架构最大问题。

还有一种问题:实体需要与具体技术架构打交道,比如发送Email,肯定要借助技术比如JavaMail发送吧,发送eMail总不能直接写在实体中吧?这些一般都写在服务中,这种服务和上面SOA服务类型不一样,可以归为领域服务,是领域模型的服务者,伺候领域模型的。

解决办法?

最直接想到方法就是将这两种服务注射到实体,使用CDI或依赖注入。

但是,问题来了,一个实体在不同场景下会有各种功能,比如正常有修改功能,然后又有被顶 被浏览功能,等等。

实体实际是一个智能机器人,有手和脚这些基本属性和基本行为,它可以参与不同活动,在不同活动场景中扮演不同角色,比如拆弹,或做饭,你总不能将这些一股脑都注射到实体,因为使用注射你要提供注射通道,结果实体被充满了拆弹 做饭等各种场景通道,被污染了。

解决有两个:
1.DCI架构:将服务和实体注射(通过Mixin trait也是可以看成是一种混合注射)到场景中。不是注射到实体中。

2.Domain Events领域事件是在不同场景下由实体发出事件驱动服务(通过类似异步消息机制实现松耦合),场景隐含,事件代表场景出头牵线。比如我们有一个拆弹场景,则由领域模型发出拆弹相关事件来实现的,出现做饭场景,则有领域模型发出做饭相关事件来实现。

相比场景,领域事件在代码上设计上比较散,没有通过场景这种类似facade类来进行归拢,象OS/J这种语言通过Team这种类似场景对相关类进行封装,实现模块化,使用OSGI实现,不失为一种好设计。

我这里领域事件定义是:由领域模型发出的事件,见Domain Events – 救世主。至于“场景行为也会触发事件”就不属于我这里提及的领域事件,那是一种通用事件模式了。领域事件属于通用事件模型一种特殊事件。

2010年03月28日 09:28 "banq"的内容
解决有两个:
1.DCI架构:将服务和实体注射(通过Mixin trait也是可以看成是一种混合注射)到场景中。不是注射到实体中。

2.Domain Events领域事件是在不同场景下由实体发出事件驱动服务(通过类似异步消息机制实现 ...

呵呵,我目前采用的是这两种方案的结合。

还有一个问题需要讨论。举个列子,比如一个人的日常行为。吃饭,睡觉这属于人这个模型本身的行为,吃饭睡觉这两个行为涉及的属性属于实体本身固有的属性。而一个人以系统架构师的角色进入公司对系统进行架构设计,而他在公司里面的架构设计的行为就属于场景行为。无论是吃饭,睡觉,还是系统架构,这些行为里面都会触发事件,那么人的固有行为(吃饭,睡觉)和场景行为(以架构师的角色在公司做系统架构)触发的事件怎么区分。我觉得他们应该都是领域事件吧?只不过在没有场景之前,做系统架构和吃饭睡觉,我们都统一在人这个实体实现了,有了场景以后,系统架构这个行为就从实体剥离出来,放入场景之中,当然即使剥离出来了,系统架构还是属于实体的行为,不过这个时候是场景行为而已。

说到这里,让我想起了设计模式。设计模式中也讲究组合优于继承,这里固有行为就是整个类体系继承下来的,而其它的行为,就要通过组合来动态的添加进来,而场景行为就属于组合范畴。

对象在大的领域范围内具有该领域的基本职责,在领域内因担当不同的角色参与各场景具有不同的职责,此时就具有参与场景的特定职责,一旦完成该场景交互则结束了相应承担的职责。T.Reenskaug在1996年发表的OORAM(Object-oriented role analysis and modeling)方法就阐述了角色职责和场景交互关系,因此前并未出现一种可直接将角色模式编写为程序的合适的编程语言,所以一直未能优雅解决此类问题,比如前面banq提到的注射污染。基于OORAM和AOP,在2001年开始研发的ObjectTeams/J(http://www.eclipse.org/objectteams/)语言填补这一空白, 它允许开发者基于场景分析,使用非常直观的角色和base类绑定实现来获得精确的聚合。当采用callout绑定角色和实体(base)对象时,当team(场景)类中role(角色)类相互协作完成交互后,角色通过callout注射到实体中的职责就自然解除了。

相关资料参见:

http://www.objectteams.org/publications/Poster_OT_Feb08.pdf OT/J 一张图概览。

http://www.objectteams.org/publications/NODe06.pdf OT/J 理论及实践分析。

2010年03月28日 20:53 "pinghe"的内容
对象在大的领域范围内具有该领域的基本职责,在领域内因担当不同的角色参与各场景具有不同的职责,此时就具有参与场景的特定职责,一旦完成该场景交互则结束了相应承担的职责 ...

是的,我们将这些与场景相关的职责和结果数据可以称为场景行为和场景属性,当然场景行为实际就是DIC中的I:交互行为。

谢谢pinghe 为我们推荐了OS/J,它的DCI实现方式是我们之前讨论的领域事件 场景对象之外第三种方式,它的callin和callout实际利用了OSGI的Active机制,OSGI白板模式Whiteboard Pattern实际也是一种事件模式,见这里之前讨论

而且OSGI的bundle可以看成是一个场景对象,类似Akka中的Trait,或Qi4j中的Mixin。它们应该都是一种事件+场景对象的架构,和xmuzuy的构思是一致的。

但是Jdonframework的Domain Events有些不一样,在静态编码阶段,没有固定的一个场景类,而是完全在运行时,通过领域模型发出Domain Events驱动相应的场景行为接口来完成(messageListener接口)。

我一直思考的问题是:是否有必要在静态编码阶段搞个场景类?类似Akka的Traint,类似Qi4j的Mixin?因为SOA的服务实际就是场景,MVC的控制器也是场景(实际可能就是从不同角度的称谓罢了,就象人有小名和大名一样),场景应该是无形的,是否有必要显式声明出来?

特别是ZK的一个叫GenericComposer 组合者,如下,怎么看也就是MVC的controller。


@Named
@SessionScoped
public class HelloWorld extends GenericComposer implements Serializable {

@Inject @ComponentId("guestName") Textbox guestName;
@Inject @ComponentId(
"sayHelloBtn") Button sayHelloBtn;
@Inject @ComponentId(
"helloWindow") Window helloWindow;

public void sayHello(@Observes @Events(
"sayHelloBtn.onClick") MouseEvent evt) {
helloWindow.setTitle(
"Hello " + guestName.getValue());
}
}


2010年03月29日 12:10 "banq"的内容
我一直思考的问题是:是否有必要在静态编码阶段搞个场景类?类似Akka的Traint,类似Qi4j的Mixin?因为SOA的服务实际就是场景,MVC的控制器也是场景(实际可能就是从不同角度的称谓罢了,就象人有小名和大名一样),场景应该是无形的 ...

呵呵,这个问题我也一直在思考。我现在是没有办法了,显示的搞出来了Context对象,我在想能否从场景的生命周期和模型的生命周期,事件的生命周期再考虑考虑。

2010年03月28日 09:53 "xmuzyu"的内容
而他在公司里面的架构设计的行为就属于场景行为 ...

不太理解为什么属于场景行为?

个人认为这个更应该属于角色行为,场景本身没有行为,但应该有场景事件,场景属性

引入场景的概念,很有创新,也符合现实世界的描述。

在UML中画usecase图,总没有什么好画的感觉。个人感觉这个“场景”的概念,把UML中的usecase进行了颗粒的细化。当然这里的场景的组合或是叠加不能等同usecase。

感觉这个是事件驱动模型,个人认为模型应有自己的行为,而在场景则是融入角色后应有的行为,不过在上面看到没明白的地方是为什么Event要去执行驱动场景呢,而Story模型中的digg的行为不属于此模型,这样感觉内聚性不高,文章属于被顶的,感觉应放入场景行为中,不知道是否正确


public class Story implements Diggable {


public void digg(EventHandler eventHandler,DiggStoryContext diggStoryContext){
DiggStoryEvent diggStoryEvent = new DiggStoryEvent(this,user);
diggStoryEvent.setDiggContext(diggStoryContext)
diggStoryEvent.accept(eventHandler);
}
}

2010年03月26日 21:46 "xmuzyu"的内容
场景和事件是不能强耦合的 ...
,看到场景中的代码还是与事件发生了耦合关系,如果事件在没有场景或者场景没有事件的情况是不是就无法工作了。

2010年05月26日 11:42 "@spawnyy"的内容
个人认为这个更应该属于角色行为,场景本身没有行为,但应该有场景事件,场景属性 ...

很长时间没有思考这个问题,最近在搞JdonFramework6.4时,又在纠结事件和场景的问题。

DCI是让我们的核心模型更加简单,只有数据和基本行为。业务逻辑等交互行为应该在角色模型中,在运行时的场景,将角色的交互行为注射到数据模型中。

领域事件属于一种业务行为,它属于DCI中I交互,而交互行为应该在角色模型中,所以,领域事件的生产者应该是角色。

据此,我写了在JF 6.4情况下的DCI实现,供参考:
http://www.jdon.com/jivejdon/thread/42529#23136338

这个案例代码和xmuzyu 的代码有些区别,区别关键是事件应该由角色发出,而不是场景。

以上只代表探索观点。