本文共 13032 字,大约阅读时间需要 43 分钟。
在过去几年中,Enterprise JavaBeans(EJB)确实已经开始对 Java 对象设计产生影响。期间,我们看到的最常使用的 EJB 模式之一是Session Facade 概念。
在过去几年中,Enterprise JavaBeans™(EJB)确实已经开始对 Java™ 对象设计产生影响。期间,我们看到的最常使用的 EJB 模式之一是 Session Facade概念。这是一个让很多开发者都受益匪浅的既强大又非常简单的概念。然而,我也看到,对这一模式的确切含义及其在实践中的应用,人们仍有很多误解。
为了把这个问题讲得更明白些,我会在本文中讲述 Facade 的一些基本概念以及Session Facade 模式的工作机制,并探讨该模式衍生出来的一些问题。希望能借此澄清一些误解,并帮助开发者正确使用这种模式。
什么是Session Facade?您又为什么需要它?
很 多地方都有对Session Facade 模式的清楚描述,也就是 [Sun 2001] 和 [Brown 2000]。我不想照抄那里的全部内容,而打算把它的理论在此作个总结:基本的问题是在 EJB 设计中,EJB 客户机(例如,Servelet、Java 应用程序,等等)不可直接访问 Entity bean。
之所以如此,有以下几个原因:
大 多数设计师已经发现为了在 EJB 设计中避免直接访问Entity bean 的解决方案都可以在 [Gamma] 中描述的 Facade 中找到。[Gamma] 这样描述 Facade 模式:“为子系统中的一套接口提供了一个统一的接口。Facade 定义了一个更高层次的接口,使子系统更容易使用。”
但问题是有很多人到此就打住了。然后 他们轻松地往下做,开始把Entity bean 包装到Session bean 中,而不考虑 Facade 模式所描述的其它内容以及 EJB 设计中由 Facade 模式衍生出来的问题。这很可能是由于把得到的 Facade 的“二手”信息都当真,而没去研究原始模式的缘故。如果我们确实花了些时间去理解 Facade 衍生的问题,我们将可以看到很多该模式所固有的其它有益的设计可能性。
Facade 模式的要点
[Gamma] 中描述了很多我们应该了解的 Facade 模式的要点。前面几点可在 Facade 模式的“适用性”描述部分找到,它描述了在什么情况下您会需要应用该模式。它们是:“当您想为复杂的子系统提供一个简单接口时……请使用 Facade 模式”和“当您想把子系统分层时……请使用 Facade 模式。使用 Facade 为每一层子系统定义一个入口点。”
从对 Facade 模式的讨论中,我们可以提炼出两个观点。第一点是 Facade 应该提供子系统的一个抽象视图,而不是简单地把整个子系统本身的 ApI 直接包装起来。不幸的是,我在实际中多次看到开发者创建的Session bean 把Entity bean home 和Entity bean 对象的全部方法直接包装起来,而不提供任何额外的抽象,这是对该模式最可恶的滥用情况之一。请记住,这种思想是想降低整个系统的复杂性,而不是把复杂性转 移到另一个对象上。
第二点,也是更微妙的一点,与分层有 关。这个观点认为您可以用多重 Facade 来隐藏下层子系统的细节。因此,在这里您可以这样设想,Session Facade 应该在其它 Facade 之上,位于最上层,是对底层业务逻辑细节的进一步抽象。这一点很关键。当您看完下面两条(分别出自 [Gamma] 中论述 Facade 模式的“协作”和“相关模式”部分)叙述后,就会更加清楚这一点:
“客户机通过把请求发送给 Facade,再由 Facade 把请求转发给适当的子系统对象来与子系统通信。” “facade 只是对通往子系统对象的接口进行抽象以使它们更易于使用;它不定义新功能。”我把这几点总结如下: Facade 不做系统的实际工作;而是委托其他对象轮流做这个工作。由此推理出您必须正确地放置这些对象,以便使该模式能按照您所期望的运行。
这一点是本模式的两种流行表达 [Sun 2000] 和 [Sun 2001] 之间的主要不同之处。第一个版本,即 [Sun 2000],是 J2EE 规划的一部分,它把这种模式称为“Session Entity Facade”。它意在表明“为一堆企业 beans 提供单一的接口”。它描述了这样一种模式,即所有的数据存取都通过Entity bean 来完成,Session bean 则为这些Entity bean 提供接口。现在的问题是 [Sun 2000] 不一定非要以 EJB 为中心。它根本不涉及其它对象类型,并且假设系统中只有 EJB 一类对象。根据我的经验,我认为这会导致根本不能在工程间重用的臃肿的Session对象,而且,在同一个工程内,当需求有一点不同时就会出现问题。
现在,[Sun 2001] 则更通用,也没有上述问题的困扰。它简单地把这种模式称为“Session Facade”。它的解决方案规定您应该“把Session bean 当作 facade 来用,以封装参与工作流的业务对象之间的交互操作的复杂性”。它根本不限制您的业务对象应该为 EJB,因此是一个更加灵活的方法。
Session Facade 的重要规则
那么我们该如何应用这些关于针对会话的 Facade 的规则呢?这对我们的 EJB 设计又意味着什么呢?我在设计Session Facade 时遵循三条基本原则:
它们自己不做实际工作;它们委派其它对象做实际工作。这意味着Session facade 中的每个方法都应该很小(异常处理逻辑不计算在内,代码应为五行或更少)。 它们提供简单的接口。这意味着 facade 方法的数量应相对较少(每个Session bean 中仅有约 24 个)。 它们是底层系统的客户端接口。它们应该把特定于子系统的信息封装起来,并且不应该在不必要的情况下公开它。那么它的工作机制呢?您还能代理别的哪些类型的对象呢?这又会给您的设计带来什么好处呢?在我的一篇 和 [Brown 2001] 这本书中,我已论述了其中一些问题,在那里可以找到一些详细信息。但,总的来说,在我的多数 EJB 设计中我通常会找到以下四类对象:
一个 EJB 对象示例
描述类似这样的模式遇到的一个问题是,能够使用这种模式的示例都太大,以至于无法包含在模式自身的描述中。尽管如此,我还是要尝试举出如下示例(它显然很简单)来说明一下这些对象看起来是什么样子。
假设我们正在为银行构建一个 ATM 系统。这是最老掉牙的 OO 设计问题之一,当然其它很多书籍和论文已经讨论过它,但它确实有足够符合我们要求的有趣特点。通过分析,我们发现了两种 EJB。
ATM Session bean 的远程接口如下:
package com.ibm.bankexample.ejbs; import com.ibm.bankexample.domain.*; /** * This is the Enterprise Java Bean Remote Interface * for the ATM example. */ public interface ATM extends javax.ejb.EJBObject { void deposit(java.lang.String accountNumber, double amount) throws java.rmi.RemoteException, com.ibm.bankexample.domain.FactoryException; java.util.Vector getAccounts(java.lang.String userid) throws java.rmi.RemoteException, com.ibm.bankexample.domain.FactoryException; void transfer(java.lang.String fromAccount, java.lang.String toAccount, double amount) throws java.rmi.RemoteException, com.ibm.bankexample.domain.InsufficientFundsException, com.ibm.bankexample.domain.FactoryException; void withdraw(java.lang.String accountNumber, double amount) throws java.rmi.RemoteException, com.ibm.bankexample.domain.InsufficientFundsException, com.ibm.bankexample.domain.FactoryException; } |
同样地,帐户 EJB 的远程接口如下:
package com.ibm.bankexample.ejbs; /** * This is the Enterprise Java Bean Remote Interface * for the Account Entity EJB. */ public interface Account extends javax.ejb.EJBObject { void deposit(double amount) throws java.rmi.RemoteException; java.lang.String getAccountNumber() throws java.rmi.RemoteException; double getBalance() throws java.rmi.RemoteException; java.lang.String getUserid() throws java.rmi.RemoteException; void setBalance(double newValue) throws java.rmi.RemoteException; void setUserid(java.lang.String newUserid) throws java.rmi.RemoteException; void withdraw(double amount) throws java.rmi.RemoteException; } |
现在,我们还发现有另外两种对象类型对我们的系统是有用的。第一种是描述显示在 ATM 机上的帐户信息的值对象。这个类看起来如下所示:
public class AccountValue implements java.io.Serializable { private java.lang.String accountNumber; private double balance; } |
当然,AccountValue 类也有作为属性的 getter 和 setter 的方法,但我们暂时不考虑它们。
现在,我们基本上有了足够的信息来理解 ATM EJB 的getAccounts()方法的实现。这个方法的实现如下:
public java.util.Vector getAccounts(String userid) throws FactoryException { AccountFactory fact = new AccountFactory(); Vector result = fact.getAccounts(userid); return result; } |
这个方法展示了Session Facade EJB 的方法的标准模式。它找到合适的帮助对象(Action 或 Factory,在本例中是 Factory),调用帮助对象上的业务方法,然后返回结果。
如这个方法所指出的,我们需要一个 AccountFactory 类来从 Accounts 构建 AccountValues。这个类的类定义如下:
public class AccountFactory { private static AccountHome accountHome = null; } |
AccountFactory 的getAccounts(userid)方法的实现如下:
public java.util.Vector getAccounts(String userid) throws FactoryException { try { Vector vect = new Vector(); AccountHome home = getAccountHome(); Enumeration accountRefs = home.findByUserid(userid); while (accountRefs.hasMoreElements()) { Account acc = (Account) accountRefs.nextElement(); AccountValue valueObject = new AccountValue(); valueObject.setAccountNumber( acc.getAccountNumber()); valueObject.setBalance(acc.getBalance()); vect.addElement(valueObject); } return vect; } catch (Exception e) { throw new FactoryException( "Cannot generate accounts due to wrapped exception " + e); } } |
这个方法使用一个高速缓存的 AccountHome 实例,它是从以下方法中获取的:
private AccountHome getAccountHome() { if (accountHome == null) { try { java.lang.Object homeObject = getInitialContext().lookup( "com/ibm/bankexample/ejbs/Account"); accountHome = (AccountHome) javax.rmi.portableRemoteObject.narrow( (org.omg.CORBA.Object) homeObject, AccountHome.class); } catch (Exception e) { // Error getting the home interface System.out.println( "Exception " + e + " in createTimeSheetHome()"); } } return accountHome; } |
正 如 [Brown 2001] 和 [Gunther 2000] 所描述的那样,在 WebSphere® 中,高速缓存 EJB home 是一个极好的习惯,因为获取 JNDI InitialContext 和从 InitialContext 获取 EJB Home 需要一段时间。
既 然您已经看到了Session、Entity和 Factory 如何组合在一起,那我们就来看一个 Action 类的示例。在本例中,我们有一个处理从一个帐户到另一个帐户的资金转移的 Transfer 对象。Transfer 由 ATM EJB 中的transfer()方法的实现中创建,该方法的实现如下:
public void transfer(String fromAccount, String toAccount, double amount) throws InsufficientFundsException, FactoryException { Transfer trans = new Transfer(); trans.transfer(fromAccount, toAccount, amount); } |
请再次注意同样的流程。不过,这个方法不必从 Action 对象返回值。Transfer 类的定义如下:
public class Transfer { private static AccountHome accountHome; } |
transfer()方法的实现如下:
public void transfer(String fromAccount, String toAccount, double amount) throws InsufficientFundsException, FactoryException { try { Account from = getAccountHome().findByprimaryKey( new AccountKey(fromAccount)); Account to = getAccountHome().findByprimaryKey( new AccountKey(toAccount)); if (from.getBalance() < amount) throw new InsufficientFundsException(); to.deposit(amount); from.withdraw(amount); } catch (Exception e) { throw new FactoryException( "cannot perform transfer. Nested exception is " + e); } } |
您 已经看到,Transfer 对象中的transfer()方法处理以下细节:定位两个 Account 实体,确保“From”帐户有足够的余额,把转移金额存入“To”帐户,从“From”帐户中提出转移金额。同样地,您可以看到 Action 对象的其它方法可以实现您系统中的其它业务规则。
使用 EJB 对象的原因
那 么为什么我们需要这第二层对象呢?难道我们从 CORBA 和 RMI 转到 Enterprise JavaBean 就使事情更简单了吗?为什么不把所有的逻辑都放到 EJB 中呢?这有几个原因。第一个也是最重要的原因是这是一个分层应用程序。在单个对象中放置太多工作从来不是一个好主意。如果您用这种方式来布置由 EJB 调用的对象,可以带来以下好处:
同 时,通过回顾几个工程,我们发现重用只在少数情况下会出现在会话层上。这是因为,每个会话都有针对一个特定应用程序的一个事务设置与方法的特定组合。有了 第二层对象则使您可以在内层级别上进行重用,我们在很多工程中都看到过这种重用,包括在企业的一个工程内(在不同的Session bean 之间)和多个工程之间。
我们已经看到,如果采用这种策略,那么您的设计经常可以把无状态Session bean 当作 Facade 对象使用。由于对某个用户而言,每个无状态Session bean 都不是唯一的,这就使您能够得到无状态 bean 所提供的另外的可扩展性。
既然我们已经知道了 facde 背后对象的类型,我们就可以开始看看 facade 能对外提供哪些类型的方法了。我们看到 Facade 方法通常属于以下几种类型:
Session Facade 的创建规则
既 然您看到了Session Facade 接口的样子,在Session Facade 背后的又有哪些对象,那么下一个问题就是“我该有多少Session Facade 呢?”您不应拥有太多的Session Facade,否则,就丧失了 Facade 模式带来的好处。但是,若整个应用程序对应一个Session Facade 它可能会成为一个“巨大的对象”
从您的应用程序中找出功能子系统。例如,名为定单管理、帐单管理和运输管理的子系统就是一个应用程序中可能有的三个 Facade 对象。 回到您的用例(Use- case)和相关的用例组。一组相关的用例 (例如购买股票、出售股票和获取报价)可以形成一个内聚子系统,例如“股票交易”。这个内聚子系统就可能要共享很多内层对象,并且是使用Session Facade 的最佳候选。[Sun 2001] 更深入地讨论了这个问题。 千万不要把所有的单个用例都做到Session Facade 中。这将导致系统分布截面过大。这种情况下,客户机将不得不管理非常多的 EJB 引用和 Home。 完成初步工作后,看一下您的设计中第二层对象之间的关系。如果您看到有值对象、Factory 和 Action 的不相交组,那就根据实际分组法把 Facade 分成两个或更多 facade。另一种会话模式
既然您已深信应该使用无状态的 Session Facade 来包装数据源,也应该在这些 facade 的实现中进行严格的分层,您可能会想知道特定于用户的应用程序状态(如果有的话)将被保存在什么地方。根据我所主张的把Session facade 设计成能处理多用例请求,您可能还会想知道用例在哪里实际实现。
为了理解如何应用它,我们采用出自 [Jacobson 1999] 的 Control 对象的如下定义:“Control 类代表协调、顺序事务以及对其它对象的控制,常用于封装与特定用例相关的控制。”
这个解决方案的目标是实现专门的、与 一个单一的用例一一对应的“用例控制器”对象。每个用例控制器都将维护一些特定于用户的状态,这些状态与判断一个用户在此用例中位于哪里所必需的信息(例 如,它们已经执行了多少)相对应,也与用户输入的、对执行用例的下一步来说必需的信息相对应。这样,我们就找到了一个能够用真正不依赖于 GUI 的方式来实现和编写用例的方案。
那么,实现这些用例控制器对象,您有哪些选择呢?因为它们天生就必须是有状态的,我们有两个 J2EE 体系结构中自己提供的选择:
一个选择是把用例控制器变为可 序列化对象(JavaBeans),然后在 HttpSession 中存储和检索它们。当然,这只能用在使用 Servlet ApI 的应用程序中。但它可以应用于很多应用程序,甚至包括那些使用 Applet 和 Application 的客户机 GUI 的应用程序,因为即使在这些情况下,通过 HTTp(也可能使用 SOAp)来和服务器端对象进行通信也是一个常用的设计策略。用 HttpSession 作为存储机制会使程序员更轻松,因为在多数应用服务器(例如 WebSphere)中会话的持久和故障转移细节都是自动处理的。然而,在 WebSphere 中,一个 HttpSession 要得到合理的持久性,则其大小将受到一定的限制(请参阅 [Gunther]),所以这个选择只适合用例控制器对象相对较小的情况。 另一个选择是把有状态的 Session bean 当作用例控制器用。有状态的Session bean 是个很好的解决方案,它使程序员不必担心如何维护会话信息。在有状态的Session bean 被自动存储在共享持久存储器的应用服务器中,这个解决方案甚至能处理故障转移。但是,在像 WebSphere 这样的应用服务器中的Session bean 并不存储在共享持久存储器中,如果碰上服务器崩溃,这个解决方案将导致用户会话失败。而且,以这种方式使用有状态的Session bean 会使我们回到增大总的分布截面的老问题上。尽管如此,由于有状态的Session bean 自己充当一个 facade,每个客户机只需知道少数的用例控制器,这样就减轻了这个问题。绕了个圈,这种模式又把我们带回到我 们最初讨论的 Facade 模式。这种模式相继分层的方式使我们可以集中精力处理手边的工作,同时还允许我们重用一些东西来构成我们的解决方案。我们不必一次性设计出整个系统,但可 以反复使用这些模式,然后看着一套可重用的对象作为结果出现。
总结
我在本文中简要分析了构建 Session Facade 的一些规则。您已经看到了该如何组织Session Facade 所调用的对象,如何着手设计Session Facade。您还看到了设计能与Session facade 配合得很好的用例控制器的一些方法。我希望,这些规则的应用能提高您的 EJB 设计的适应性和性能。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-130499/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/374079/viewspace-130499/