EJB 倡导者: 使用何种 EJB 组件组装来自服务的数据

EJB 倡导者对面向服务的体系结构从上至下进行了分析,以最终确定应该使用会话 EJB 组件还是实体 EJB 组件组装服务返回的数据传输对象。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --> <!--END RESERVED FOR FUTURE USE INCLUDE FILES-->

在每个专栏中,EJB 倡导者提出要点,客户和开发者采用独特的前后衔接的对话框方式,在对某一感兴趣的设计问题提议解决方案的过程中进行交流。其中忽略了任何确定性的细节,并且不提供“革新的”或专有的体系结构。有关详细信息,请参阅 EJB 倡导者简介

实体 EJB 组件在服务中的角色

前一篇专栏文章中, 我们介绍了面向服务的体系结构的一些特征;例如,为了尽可能减少服务和其客户机间频繁的对话,服务必须为粗粒度的无状态服务,而且必须正常组装数据传输对 象 (DTO),此类对象会收集所有从客户机应用程序返回的属性。我们了解了设计良好的 EJB 方法如何展示这些特征(通过与会话 EJB 实例或实体 EJB Home 接口关联)。不过,在本月,有位读者询问我们是否认为实体 EJB 组件也设计为无状态服务。


问题:哪个数据传输对象由哪个实体返回?

亲爱的 EJB 倡导者:

我已经读过您最近的文章,对于数据传输对象 (DTO),我想提出一些自己的看法。我同意您所说的,数据传输对象在服务层(该层中需要各种粗粒度方法)非常重要,特别对于表示隐藏实现的服务的外观,尤其是远程外观(因为远程调用的开销要比本地调用大得多)更是如此。

此 外,推荐的代码看起来非常合理,并得到了良好封装,能够让您要求实体 EJB 组件“给我一个您自己的 DTO”。不过有一个问题,对于两种不同的用例,一种可能需要给定实体 EJB 组件的某些属性,而另一种则可能需要不同的属性,因此会需要两个 DTO 类。那么,getDTO() 方法应该返回哪个 DTO 类型呢?

还有另一个问题。不同的用例可能需要由 相同类型或不同类型(客户、延期交货订单、订单中的产品)的多个实例组成的 DTO。这种情况下,需要在哪个实体 EJB 中实现该复合功能?

在 会话外观中,似乎不能避免对实体 EJB 组件使用单个 getter(和 setter)。将组装 DTO 的逻辑放入到会话 Bean 中(或者,按照我喜欢的方式,放入会话使用的 Helper 类中)的好处之一就是,当由于某种原因(如添加另一个服务所需的新属性)需要删除或重新生成实体时,不用担心自定义代码发生不同步的现象。

我想,我真正想问的是:您在建议使用实体 EJB 组件的本地接口时,从实体 CMP 返回 DTO 是否是在“喊狼来了”?我理解您关于尽力减少各层之间的频繁对话的观点,但似乎您在以牺牲可维护性为代价来换取性能的提高。

请用这个名字称呼我们,
Never Cry Wolf


从良好封装技术获得一项好处

亲爱的 Never:

您提出了很好的观点,非常值得加以说明。让我们从上到下依次回答您提出的问题:

  1. 假设两种用例需要不同的数据,getDTO() 方法应该返回哪个 DTO 类型?

    我的回答是:getDTO() 当然应该返回 DTO 的实例。认真一点讲,您为 DTO 选择的名称应该清楚地指示它所返回的数据。如果两种用例需要不同的数据集,DTO 类名称应该指示这一点:可以为 UseCase1DTO 或 UseCase2DTO 之类的。因此接口中的两个方法签名将为:


    代码段 1. DTO 方法的多个签名
    						
    public UseCase1DTO getUseCase1DTO();
    public UseCase2DTO getUseCase2DTO();

    或者,更抽象一些:


    代码段 2. 显示抽象示例的更不易混淆的方式
    						
    public <DTO> get<DTO>();

    约 定的最后一个要点是:如果要让 getDTO() 方法与实体关联,一种方法就是为返回 <Entity>DTO 的实例的方法将保留此方法名称,该 DTO 包含关联的实体的所有非标量属性(这些属性的最大基数为 1)。您的团队可能选择使用另一个约定,只要确保一致性即可。

  2. 假设需要来自多个实体(如客户、延期交货订单、订单中的产品)的数据,哪个实体应“拥有” get<ComposedDTO>() 方法?

    要从定义实体 EJB 上的复合 DTO 方法的角度回答这个问题,就要涉及到面向对象的设计的最佳实践。我们在专栏文章 使实体 EJB 组件满足需求,第 2 部分 中简单讨论了 OO 委托。对实体的选择取决于实体与要返回的数据之间的关系。通常,需要选择可以根据需要委托到其他实体以组成完整结构的实体。在您所给出的具体例子中,您希 望 DTO 返回来自客户、延期交货订单、订单中的产品(由一个 Line Item 表示,其中包含数量和总数的链接属性)的数据。让我们暂时假设图 1 中的类关系图描述了业务对象及其相互关系。


    图 1. 显示实体及关系的示例类关系图
    图 1. 显示实体及关系的示例类关系图

    对 于您的示例,“拥有”复合 DTO 方法的实体理所当然应该是 Customer,因为它能够委托到 Order,而后者能够委托到 Line Item,Line Item 又能委托到 Product。对于 Customer 实体上的方法名称,可以选择与 getCustomerOpenOrderDetails() 类似的名称:。

    关系图上显示的关系启用了多个路径,我们可以通过这些路径检索组成所有四个实体类型的信息的 DTO。例如:

    • 给定一个客户,检索延期交货订单的详细信息(上面所描述的)。
    • 给定一个客户,检索所有相关订单的详细信息。
    • 给定一个订单,检索其客户和详细信息。
    • 给定一个行式项目,检索其相关的产品,包含此行式项目的订单和客户。
    • 给定一个产品,检索所有相关的订单行式项目和客户。

    拥有方法的实体在上面的“given a(n) <Entity>”子句中指示,它表示起始点,因此假设如下:任何与此方法关联的服务外观(或 EJB home 方法)都将采用能标识用于开始委托的一个或多个实体类型的参数。希望这能够回答您的第二个问题。

  3. 当建议从实体 EJB 返回 DTO 的同时建议使用本地接口,是否是在“喊狼来了”(以牺牲可维护性为代价换取性能的提高)?

    在 EJB 1.x 的早期,我们也进行过与此类似的有趣的得失分析。我们发现对于从实体返回数据,存在两种极端方法和一种中立方法,从而得到三种基本的访问方式:

    1. 一次一个属性
      从 表面上看,可维护性最好的方法就是完全避免 DTO,而采用一次从客户机获取一个属性的方法。但这却是以减少消息数目和大小为代价获得可维护性。一般用例将以许多小消息结束,当涉及到远程接口时,端 到端通信开销会对响应时间和吞吐量造成很大的影响。另一个不足(正如您所提到的)就是,还破坏了封装。破坏了封装会对可维护性造成负面影响。例如,如果决 定将一组属性构造到相关实体中,将需要更改使用这些属性的每个客户机!
    2. 一个大小适合所有 DTO
      另 一种极端的方法就是返回与在 DTO 中返回的目标实体关联的所有非标量数据,正如我们在回答第一个问题时所讨论的。在这种情况下,您最小化了远程消息的数目,同时仍然具有相对的可维护性。只 对每个实体调用一次,并一次获取所有能获得的全部数据。这样您只得为每个实体开发一个 DTO 和一个检索方法。当然,折衷方案是 DTO 中包含的数据几乎始终多于给定工作单元所需的数据,在通常具有很多属性的实际实体中更是如此。因此,此方法对为每个工作单元从后端数据系统检索和传输的数 据量有很大的影响。这种方法的好处在于,当每次添加或删除属性时,只需要修改 DTO 即可。而只有客户机使用已删除的属性时才需要对其进行修改。
    3. 自定义 DTO
      中 立方法(EJB 倡导者想起了《金发姑娘与三只熊》的故事)就是对 DTO 结构进行定制,使其准确返回您所需要的数据。一次调用。大小合适。真不错。但问题在于,您必须对用例进行仔细的分析,以获得恰当的一组 DTO 和方法。当实现用例的新类时,必须创建一个新的 DTO,并调整 EJB 以对其进行检索。

    当使用 EJB 1.x 实体时,选项 c 被认为是最好的,因为分布式对象应用程序设计主要就是为了尽可能减少调用(可能为远程调用)的数量。选项 b 是一个不错的折衷办法,能获得一定的可维护性。这样一来,就变得更简单了,再没有烦人的 CMR,也不用考虑“依赖对象”(属性的集群,实际表示“包含”在实体中的对象)以外的东西。

    但现在已经推出了具有本地接口 的 EJB 2.x 和 CMR,我曾想过选项 a 是不是最佳的方法(我们刚在专栏文章 Making entity EJB components perform, Part 1 (LINK) 中提到过这个问题)——但我们始终认为,出于同样的性能原因(尽管它们可能不那么重要),使用选项 c 中描述的自定义 DTO 仍然是最好的。我们还指出,由于很多人都已经习惯了 EJB 1.x“最佳实践”,比起对他们进行重新培训而言,这更简单(而且这也使转换变得更容易)。

希望这会对您有所帮助,
您的 EJB 倡导者


旧话重提,可维护性问题

亲爱的 EJB 倡导者:

我理解并同意您对前两个问题的回答。这让我受益非浅。关于您对我的第三个问题的分析,我的理解是,为什么您认为对于 EJB 1.x,从性能的角度而言,选项 c 最佳,但您并没有真正回答如果使用 EJB 2.x 如何解决可怕的可维护性问题。

回顾一下您的回信中的简单关系图。有五种不同的“顶级”方法对来自所有四个实体的信息加以组合。您没有提到与每个实体关联的,处理委托的方法。让我们看一看与从客户获取延迟交货订单关联的方法:

  1. 给定一个订单,返回该订单和带产品的行式项目。这将委托给:
  2. 给定一个行式项目,返回该行式项目和产品,这将委托给:
  3. 给定一个产品,返回其 DTO。

那么,存在不包括所有四个实体的组合。仅从客户来看,就可以提出三种以上的方法:

  1. 只从客户返回数据。
  2. 仅返回客户和延期交货订单(不含详细信息)。
  3. 返回客户和所有相关的订单(但也不含详细信息)。

现在,将所有这些添加到从每个实体获取部分属性的排列中。总之,这种组合非常多,尤其在考虑到(如您自己所说)通常存在更多与每个实体关联的属性时,更是如此。

因此,我不甚明白的第三个问题是,您似乎以大量的可维护性为代价,获得现在的少量性能改进。

先行谢过,但我仍然必须签上这个名字:
Never Cry Wolf


数据传输对象与视图关联

亲爱的 Never Cry Wolf:

开 个玩笑,现在是您在喊“狼来了”。您不大可能在自定义开发的应用程序中看到很多此类置换。为了理解我这样讲的原因,让我们从顶层——用例着手。尽管本文不 是关于进行面向对象分析和设计的方法的完整教程,但我们仍将简单了解一些主要构件,以说明排列的数量相对来说是有限的。

图 1 的类关系图中显示的实体与订单管理业务流程相关联。该流程可以使用状态转换图 (STD) 进行描述,如图 2 中所示,其显示了受管理的单个订单的生命周期的各个阶段。


图 2. 显示订单的生命周期的示例状态转换图
图 2. 显示订单的生命周期的示例状态转换图

顺便说一下,仅供参考,图 2 使用 UML Actor 表示法进行了扩展,以显示在给定状态中实例的所有者。我们使用这个简单的扩展已经多年了,将此作为组织用例并将其绑定到业务流程的方法(事实上,我们喜欢开玩笑地称用例关系图就是一个尚未“孵出”的状态转换图)。

我 们采用的另一个略微不同寻常的方法就是为生命周期模型中的每个状态开发一个类关系图,以显示对保持在该状态中的属性和关系的约束。这些不断更改的约束在不 考虑状态的“单态”类关系图中容易丢失。例如,图 1 中的类关系图显示的是保持在图 2 所示的生命周期的开放状态中的关系和属性。处于已提交状态的订单的类关系图可能与图 3 类似。


图 3. 显示处于已提交状态的订单的示例类关系图
图 3. 显示处于已提交状态的订单的示例类关系图

该 方法按照各个状态提供独立的类关系图,说明了为什么图 3 和图 1 中的类名使用了 [] 来指示状态,并将“动态”状态转换模型和“静态”类关系图绑在一起。通过比较图 1 和图 3,可以很容易发现随着 Order 的状态在业务流程中由开放进入已提交状态(如图 2 所示),其“形状”发生了更改。图 2 中的状态显示了行为的更改。

静态模 型和动态模型相一起定义了面向服务的体系结构中完整的服务集。生命周期模型中的每个状态都可以一对一地映射到会话 EJB 组件。STD 中的每个转换都映射到与状态关联的会话 Bean 上的方法。关系模型中的每个类均可以映射一个实体 EJB 组件。关系图中的关系自然映射到 CMR。因此对于开放和已提交两种状态,可以推断出将有两个会话 EJB,而这两个会话 EJB 分别具有三四种更新方法。对于所显示的两个类关系图,可以推断出将有 15 个实体 EJB 组件,具有许多属性和 CMR。

为了具体一些,让我们以延期交货订单状态为例进行说明。首先我们编写一个纯 Java 接口,可以在整个过程中重用此接口,如下所示:


代码段 3. 显示派生于 STD 的方法的纯 Java 接口
				
public interface OpenOrder {
OrderKey open(CustomerKey cust)
throws CustomerNotFound, OrderAlreadyOpen;
int addLineItem(CustomerKey cust, ProductKey product, int qty)
throws CustomerNotFound, OrderNotOpen, InvalidQuantity;
void submit(CustomerKey cust)
throws CustomerNotFound, OrderNotOpen, OrderHasNoLineItems;
void cancel(CustomerKey cust)
throws CustomerNotFound, OrderNotOpen;
}

此接口可以在会话 EJB 接口和类似于以下的实现中重用:


代码段 4. 会话 Bean 接口和实现类
				
public interface OpenOrderSession
extends javax.ejb.EJBLocalObject, OpenOrder;
public class OpenOrderSessionBean
implements javax.ejb.SessionBean
{
// implementations go here
}

如 果希望,您可以使用 EJB Home 方法代替会话 EJB。在这种情况下,Customer 实体就成了业务逻辑理所当然的“网关”,因为它表示的是在该状态中驱动业务流程的 Actor。在这种情况下,可以采用上面处理会话的方式仅扩展同一个 OpenOrder 接口,如以下代码段所示:


代码段 5. 重用 OpenOrder 接口的 Customer 实体 Home 接口
				
public interface CustomerHome
extends javax.ejb.EJBLocalHome, OpenOrder
{
// other Home methods like findByPrimaryKey() and create()
}

无论选择哪种方法,这些模型和映射都能提供实现更新方法所需的组件。如果要一一提供您的分析中列举的每种排列,则也可以将这些动态和静态的模型用于派生所有的读取方法。但我们发现,最好从交付支持调用业务流程函数所需的数据的用户界面 (UI) 屏幕流派生这些读取方法。

对 于为屏幕流编写文档,我们也希望使用状态转换图。可以将屏幕流 STD 看作是捕获在业务流程模型中拥有状态的 Actor 的典型“会话”的生命周期。其中的状态显示屏幕和弹出对话框,而转换显示用户启动的事件。为了加以说明,图 4 演示了客户的一个屏幕流,显示它如何与订单管理流程进行交互。


图 4. 客户订单管理会话的示例屏幕流
图 4. 客户订单管理会话的示例屏幕流

模型间的交互(如果有)在转换上的 {} 内指定,显示对业务流程调用转换时的副作用。某些事件(如提交)会进入一个确认对话状态。只有用户触发了“OK”,才会实际出现提交订单的副作用。这些确认状态用斜体表示。

另 一个用斜体显示的状态就是“Home”状态,该状态表示角色(本例中为 Customer)如何启动和结束其会话。还有一个用斜体显示的特殊状态与业务流程相关联——因为给定的 Actor 可能与多个流程交互。这两个状态都很特殊,因为它们只进行导航(它们通常以菜单或选项卡的形式出现,具体取决于 UI 的样式)。

其 他的状态都表示“实际”的屏幕(或屏幕的一部分;由于这已经涉及到一般 J2EE 最佳实践,所以我们将在另一篇文章中专门就此进行讨论)。对于每个屏幕(不管是否为特殊屏幕),都可以使用类关系图捕获该状态的可见数据,类似于和业务流 程关联的类关系图显示与状态关联的持久数据的方式。图 5 显示除确认状态之外的所有状态的组合类关系图。


图 5. 显示会话的可见数据的示例类关系图
图 5. 显示会话的可见数据的示例类关系图

现 在我们就能理解为什么不会有很多 DTO 和相关方法的原因了。图 5 显示了七个包含内容(关系计数)的 DTO。这些 DTO 非常直接地映射到 Java 类。图 4 所示的屏幕流上的到某个状态的转换映射到 OpenOrder 上的其他方法。它们所返回的 DTO 在图 5 的类关系图中进行了描述。我们要使用的约定是 <TargetState>Data(而非 DTO)。此方法的参数作为与转换关联的数据流显示;其名称将成为方法名的一部分。代码段 6 显示了某些示例:


代码段 6. 某些从 UI 派生的只读方法
				
CustomerHomeData getCustomerHomeData(CustomerKey cust)
throws CustomerNotFound;
ProductCatalogData getProductData(CustomerKey cust)
throws CustomerNotFound;
ProductCatalogData getNextProductData(
CustomerKey cust,
ProductKey last
)
throws CustomerNotFound, ProductNotFound;
ProductCatalogData getPreviousProductData(
CustomerKey cust,
ProductKey first
)
throws CustomerNotFound, ProductNotFound;
OrderDetailsData getOpenOrderDetailData(
CustomerKey cust,
)
throws CustomerNotFound, OrderNotFound;
OrderDetailsData getOrderDetailData(
OrderKey order,
)
throws OrderNotFound;
OrderStatusData getOrderStatusData(
CustomerKey cust,
)
throws CustomerNotFound;

正 如您所看到的,只读方法非常少。“根”DTO 的数目甚至更少,因为这些 DTO 被重用。最糟糕的情况下,此例中也仅需八个 DTO 支持业务流程。而且,即使在实体 EJB 组件中进行 OO 委托以加载完整的结构,get<DTO>() 方法的总数仍将相对较少。

很抱歉,为了回答一个相对简单的问题,我讲了这么一大堆关于常规 OO 分析和设计技术的话题,但我仍然认为在实际操作中,通过返回需要的数据结构所带来的封装优势会超过相关维护问题的困扰。

您同意吗?

好,就此打住,
您的 EJB 倡导者.




旧话重提,性能问题

亲爱的 EJB 倡导者:

您进行面向对象的分析和将工作成果映射到 EJB 组件的方式很有意思。您的方法让我相信可维护性问题并非想象的那样糟。但我还有一个问题想请教。

我 仔细考虑之后,得出了一个完全不同的问题:如果调用 CMP 的 get<attribute>() 方法完全没有性能损失,您是否仍然会建议会话外观、DTO 装配器或实体 Home 方法对实体使用 get<DTO>() 方法,而不直接一次性从相关实体获得其所需的属性?

例如,以图 5 中的 DetailItemData DTO 为例。我认为,如果我按照您的建议使用 OO 委托,LineItem 实体将具有一个 getDetailItemData() 方法。此方法将按照 CMR 的指引获得对 Product 的引用。您建议不对 Product 调用 getDescription() 和 getPrice(),而是创建一个新的 ProductDescriptionAndPriceData 对象,并对 Product 调用 getProductDescriptionAndPriceData()。然后我将从此结构中复制字段,并将这些字段和 productId 与 quantity(以及计算得到的量)一并复制到 DetailItemData 结构中。

我觉得这个方法真的很多余,除非性能真的有那么糟糕。

盼复,
Never Cry Wolf


相信您的直觉,但想进行验证

亲爱的 Never Cry Wolf:

您坚持不懈(没有别的意思)的询问真的让 EJB 倡导者不禁怀疑自己倡导使用 EJB 的力度是否不够。

我 必须承认,如果对本地实体 EJB 组件使用 get<attribute>() 方法,没有任何性能问题,您所采用的在外观或实体 Home 方法或已委托的方法中组装所需数据的方法更具吸引力。这个方法在生成时要编译的组件数量更少,运行时要回收的垃圾也更少,因此,我很高兴您相信了您的直 觉,认为真正的问题并没有得到回答。我们之间的这几番交流表明,有时候必须反复多次才能彻底了解问题的实质。

另一方面,我也曾有 个直觉,认为本地方法调用的性能影响仍然很可观。但我没有采用任何有意义的方式进行测试,以验证这个想法,而是相信那句格言“if it ain't broke, don't fix it”(东西还没坏,就别急着去修它)——不仅是因为进行性能测试非常麻烦,而且也由于修改所有关于最佳实践的演示材料和现有示例比这更麻烦。

这句格言对于“早期的”应用程序非常合理,但能验证我原来的直觉则更好,因为新开发项目可以在适当的情况下选择适当的方法。

因 此,我让一个团队进行了性能测试,结果我很惊喜地发现,只要客户机和实体均位于同一个全局事务范围内,“客户机”通过本地接口调用本地 get<Attribute>() 方法和让实体 Bean 实现调用自身抽象的 get<Attribute>() 方法,这两种方式之间并没有明显的区别。

有了这个新数据后,我非常高兴地对我的方法进行了修改,如果尚没有提供信息的现有 get<DTO>() 方法,则直接使用 get<Attribute>() 方法组装所需的数据。这适合于实体 EJB、实体 Home 方法、会话外观或(如您所称) DTO 装配器类上的已委托的方法。

我还十分高兴地发现,居然有人比我还更像一个 EJB 倡导者!如果有机会见面的话,我一定会请您吃饭。

好,就此打住,
您的 EJB 倡导者.



结束语

下面是从以上讨论中得出的一些有意义的原则:

  • 您的分析工作成果应该同时涵盖业务域和用户界面的动态和静态方面。
  • 系统地将您的分析工作成果映射到适当的 EJB 组件和 DTO。
  • 会话 Bean 非常适合实现与业务流程(更新)或屏幕流(读取)中的状态关联的转换。
  • 当与表示拥有一个或多个业务流程中的状态的 Actor 的“网关”对象关联时,实体 Bean Home 方法是最好的选择。
  • 在两种情况下,都要尽可能利用与网关对象关联的 CMR,并根据需要使用 get<DTO>(OO 委托)或 get<Attributes>(过程型组装),以获取要返回的数据。这很大程度上取决于您是否已经拥有适当的 DTO。
  • 最后,EJB 倡导者愿意承认自己错了。只是要认识到自己的错误可能还需要一些时间。

我们相信您还能够找到更多的类似问题。在下一篇专栏文章推出之前,这就够您忙活的了。


致谢

在此特别感谢 Bobby Woolf,他为本文提供了重要的建议和资料。请访问 Bobby 的博客来了解 J2EE in Practice



参考资料



关于作者

作者照片

Geoff Hambrick 来自 Texas 的 Round Rock(在 Austin 附近),是 IBM Software Services for WebSphere Enablement Team 的首席顾问。Enablement Team 通常通过深层技术简报及短期概念验证为售前流程提供支持。Geoff 于 2004 年 3 月被选为 IBM 的杰出工程师 (Distinguished Engineer),他的工作是创建并传播用于开发 IBM WebSphere A

阅读更多

没有更多推荐了,返回首页