内容


Domino 的业务域驱动 Java 类的层次结构

Comments

如果您正使用 Domino Java 类开发 Domino 应用程序,那么向您推荐一个实践,即在一个公共基类集合中封装技术性 Domino 数据库实现的细节,并构建业务域驱动 Java 类的层次结构。可以借鉴一些众所周知的概念,比如 Enterprise JavaBeans (EJB) 技术中的 create 和 find 方法,来简化处理业务对象的代码。本文将提供一个小型类层次结构的例子,举例说明这个层次结构的概念。本文是为熟悉 Domino Java API、经验丰富的 Java 开发人员编写的。

简介

Domino 数据库中的文档表示业务流程中的不同对象。这些文档中的数据可能是关于客户、雇员、产品、订购单、交货报告、付费信息或者其他是业务逻辑的一部分的业务对象的。在业务工作流中,技术性基础设施的代码(比如访问 Notes 数据库、通过视图访问 Notes 文档和访问 Notes 文档中不同的项)与实现业务逻辑的代码(比如访问最近 10 天内送达某一特定客户的所有订单)可能是不同的。

为了保持业务逻辑清晰并易于维护,不要用太多的技术性代码将其复杂化。做到这一点的最佳方法是在一个小的公共基类的集合中封装特定于 Domino 的实现细节。这个面向对象的方法是首选方法,特别是在并不完全熟悉 Domino Java 类时,或者在致力于 Domino 是基于 Java 的基础设施的一部分的项目时。牢记这个设计目标,并借鉴 EJB 技术中的 create、find 和其他方法的思路,来构建业务域驱动 Java 类的层次结构,简化业务逻辑代码的实现,并最大程度地提高代码重用和一致性。

业务域驱动类的层次结构的一个简单例子

在本文的以下小节中,我们将使用一个小而简单的例子来说明这个层次结构的概念。您可以从 Sandbox 中下载本文的示例文件。

我们设想了一家叫做 ACME 的公司,该公司有一个基于 Domino 服务器基础设施的订单追踪系统。这个 Domino 应用程序被集成在基于 WebSphere Portal 的新的 ACME 客户门户中。要以有效的方式访问 Domino 数据库,则需要创建一个域驱动的类层次结构。

名为 acme.nsf 的数据库包含拥有客户数据的文档。每一个客户都可以拥有一个或者多个与其相关的订单,这些订单存储在同一数据库中的单独文档中。客户文档包含一个惟一的 ID,而订单包含惟一的客户 ID,以及针对客户惟一的订单 ID。所以,在该例中,客户 ID 以及 订单 ID 是查找特定订单所必需的。业务对象(客户和订单)共享一些公共功能,比如创建、删除、检索、更新和访问这些业务对象实体的某些适当实例的属性。

以下类图显示了类 BOBase、BOEntity、BSession、Customer 和 Order。前三个类构成了类层次结构的基类。后两个类代表了业务域驱动示例类。此外,与 Domino Java API 元素 Session、Database、View 和 Document 的关联关系也包含在该类图中,并带有适当的 <<JavaInterface>> 标记。

图 1. BOBase 图
BOBase 图
BOBase 图

存储在 Domino 数据库中的所有业务对象实体的基类是类 BOEntity。因为业务对象是在容器层次结构中构造的,所以 BOEntity 类包含指向相关 Notes 文档的实例和 UNID、业务域驱动 ID 以及相关父对象 ID(如果适用的话)的指针。容器层次结构允许您毫不费力地访问父元素中的所有子元素,例如,允许访问与某一特定客户相关的所有订单。

BOSession 类提供创建该类的实例的静态工厂方法,封装用来访问 Notes 数据库的 Notes Session 实例。这个 Notes 会话可以使用用户名和密码来实例化,也可以用 LTPA 令牌来实例化。此外,也可以实例化匿名 Notes 会话。每一个 BOEntity 实体都包含一个到其附属 BOSession 的关联。

客户类包含一个用于创建新订单的方法,以及一个查找特定订单或者查找与某一客户相关的所有现有订单的方法。而会话类包含创建新客户和查找数据库中的一个或所有客户的等效方法。所有派生实体类和会话类都继承了 BOBase 类中 create 和 find 方法的公共代码。在本文的后面部分,我们将更详细地讨论这些类。

实现的细节

为了保持示例代码的简单,所有文档都保存在数据库 acme.nsf 中,并且都通过一个叫做 lupview 的视图来访问。在这个示例中,该视图包括三个升序排列的列:

  • 列 1 包含表单名,它表示了业务对象类型,比如 Customer 或 Order。
  • 列 2 包含惟一客户 ID。
  • 列 3 包含每个客户的惟一对象 ID。

在本例中,这个查找视图由查找方法 findCustomerById()、findAllCustomer()、findOrderById() 和 findAllOrder() 所使用。以下屏幕显示了该查找视图(lupview)。

图 2. 查找视图
查找视图
查找视图

以下代码示例使用业务对象类。在使用这些类时,您要做的第一件事就是利用静态方法 createBOSession() 创建一个新的会话实例。该会话用于查找某一现有客户,以便创建一个与该客户相关的新订单,并使用适当的设置方法来设置一些属性。尽管这个示例类访问一个 Notes 数据库、一个视图和多个文档,但它不需要直接导入 Domino Java 类包,因为 Domino Java API 类封装在业务对象类中。

import java.util.*;
import acme.*;
// Example1 - Find a customer and create a new
// order associated with this customer
public class Example1 {
    public static void main(String[] args) {    
    BOSession boSession = BOSession.createBOSession();    
    try {    
        Customer customer = boSession.findCustomerById("C12345");        
Order newOrder = customer.createOrder("7");        
        newOrder.setStatus(Order.UNPROCESSED);        
        newOrder.setProduct("Chair");        
        newOrder.setQuantity(4);        
        newOrder.close();        
// further processing ...        
customer.close();
    } catch (InvalidStateException e) {    
        e.printStackTrace();
    } catch (InvalidIdException e) {    
        e.printStackTrace();
    } catch (CreateException e) {    
        e.printStackTrace();
    } finally {    
        boSession.close();
    }    
    }
}

如果客户 ID 是无效的,那么以下堆栈跟踪(stacktrace)中将抛出适当的 InvalidIdException 异常:

acme.InvalidIdException: Business Object with ID [Customer C12349] not found!
    at acme.BOBase.findByID(BOBase.java:116)    
    at acme.BOSession.findCustomerById(BOSession.java:71)    
    at Example1.main(Example1.java:13)

如果已经用现有的订单 ID 为该客户创建了一个新订单,那么将在以下堆栈跟踪中抛出适当的 DuplicateIdException 异常。类 DuplicateIdException 扩展基类 CreateException。本文的后面部分将介绍生成客户 ID 和订单 ID 的机制,以及保证这些 ID 的惟一性的机制。

acme.DuplicateIdException: Order with ID 7 already exists.
    at acme.Customer.createOrder(Customer.java:84)    
    at Example1.main(Example1.java:15)

异常类

下面的类图展示该例中使用的不同异常类。它们表示可能发生在业务逻辑处理过程中的不同错误情形,同时还表示不同于 Domino Java API 的技术驱动的 NotesException 类的抽象层次。

图 3. BOException 图
BOException 图
BOException 图

本文的后面部分将展示 DeleteException 的一个例子,并将在随后的一个小节中描述 InvalidStateException 的含意。

更改订单的状态

第二个例子将展示如何查找某一客户以及该客户的所有相关订单,为了更改状态属性,需要对它们进行迭代。这项操作是通过关闭已更改的订单来完成的。如果有任何属性发生更改,那么 close 方法会把所有更改都写回 Notes 数据库。

// Example2 - Find a customer, find all orders of this customer,
// iterate all orders and close them
...
BOSession boSession = BOSession.createBOSession(); 
    try {         
       Customer customer = boSession.findCustomerById("C12345");  
       Vector vOrders = customer.findAllOrder();        
       Enumeration enumOrders = vOrders.elements();        
       while (enumOrders.hasMoreElements()) {       
       Order order = (Order) enumOrders.nextElement();        
       System.out.println("Order Id:"+order.getId()+
          "Customer Id:"+order.getParentId()+
          " Product:"+ order.getProduct());        
       
       // further processing ...        
        
       order.setStatus(Order.CLOSED);           
       order.close();              
       }              
       customer.close();      
       } catch (InvalidIdException e) 
       {        
       e.printStackTrace();   
       } finally { 
       boSession.close(); 
       }

删除客户

第三个例子展示了如何查找某一客户名和如何删除它。在这个简单的例子中,客户类的 delete 方法包含检查该客户的一个或多个订单是否依然存在的适当代码。如果存在,则抛出 DeleteException。另一个实现可能试图分别删除数据库中的客户对象和所有相关订单对象,或者删除与适当的业务逻辑一致的其他任何实现。

// Example3 - Find a customer and try to delete it.
// If an order for this customer exists, an exception 
// will be thrown.
...
BOSession boSession = BOSession.createBOSession();
try {
    Customer customer = boSession.findCustomerById("C12345");    
    // further processing ...    
    customer.delete();     
    customer.close();
} catch (InvalidStateException e) {
    e.printStackTrace();
} catch (InvalidIdException e) {
    e.printStackTrace();
} catch(DeleteException e) {
    e.printStackTrace();
} finally {
    boSession.close();
}

以下是 DeleteException:

acme.DeleteException: Can't delete customer [C12345], 
because associated orders exist.
at acme.Customer.delete(Customer.java:100)
at Example3.main(Example3.java:18)

BOBase 和 BOEntity 类

以下两个代码示例展示了基类 BOBase 和 BOEntity 的重要部分。第一个代码片断展示了基类 BOBase。它包含用于访问查找视图的 findById 和 findAll 方法以及一个称为 createEntity 的基本创建方法的公共代码,该方法在 Notes 数据库中创建一个文档,并设置所需的项:表单名、对象 ID 和父 ID。此外,基类 BOBase 包含返回实际数据库实例的方法。在这个示例中,只用到了一个数据库,但在多数据库解决方案中,派生的业务对象类可以覆盖该方法,返回其适当的主要 Notes 数据库实例。(在这个例子中,还可以使用适当的数据库名,在称为 dbName 的派生业务对象类中声明私有 String 属性。)

方法 getLupView() 将返回查找视图的实例;BOBase 类中的所有 find 方法都可以使用该方法。在多数据库解决方案中,还可以覆盖该方法来返回主要 Notes 数据库实例的一个有效视图实例,或者数据库名的等同物。可以用适当的视图名来声明私有 String 属性。

package acme;
import java.util.Vector;
import lotus.domino.*;
public class BOBase {
 ...  
protected Document createEntity(String id, String parentId, String boName) {
    Document doc = null;
    try {
        doc = getDatabase().createDocument();
        doc.replaceItemValue(ITEM_FORM_NAME,boName);
        doc.replaceItemValue(ITEM_OBJECT_ID,id);
        if(parentId != null) doc.replaceItemValue(ITEM_PARENT_ID,parentId);
        doc.save();
    }
    catch(NotesException ne) {
    handleException(ne);
    } 
    return doc;
} 
 
protected DocumentCollection findAll(Key key) {
    DocumentCollection retval = null;
    try {
        View lup = getLupView();
        if (lup != null) {
        retval = lup.getAllDocumentsByKey(key.getEntries(), true);
    }
    } catch (NotesException ne) {
    handleException(ne);
    }
    return retval;
}
protected Document findByID(Key key) throws InvalidIdException {
    Document retval = null;
    try {
    View lup = getLupView();
    if (lup != null) {
        Document doc = lup.getDocumentByKey(key.getEntries(),true);
        if (doc == null) {
            InvalidIdException idex =
            new InvalidIdException("Business Object with ID "+key+" not found!");
            throw idex;
        }
        retval = doc;
    }
    } catch (NotesException ne) {
        handleException(ne);
    }
    return retval;
}
 ...
protected synchronized View getLupView() {
    try {
        if(lupView == null) {
        lupView = getDatabase().getView(lupViewName);
        }
        else {
        lupView.refresh();
    }
    } catch(NotesException ne) {
        handleException(ne);
    }
    return lupView;
    }	
}

以下代码示例展示了基类 BOEntity 的一部分,该类包含读写文档项、保存 Notes 文档实例(如果发生更改)、回收和删除文档的一些方法的公共代码。该类是封装 Notes 文档的所有业务对象类的父类。受保护的方法 checkState() 验证私有 Notes 文档实例指针。如果该指针等于 null,那么将抛出适当的 InvalidStateException。该方法允许在每个业务逻辑方法的开头部分设置智能守卫条件,该条件依赖于底层 Notes 文档的有效性。客户类中的方法 createOrder()(请参见下面的代码示例)将使用这个守卫条件方法。

package acme;
import java.util.*;
import lotus.domino.*;
public abstract class BOEntity extends BOBase {
 ...
protected Vector readValues(String itemName) {
    Vector retval = null;
    try {
        retval = (Vector) doc.getItemValue(itemName);
    } catch (NotesException ne) {
        handleException(ne);
    }
    return retval;
}
 ...
 
protected void writeValue(String itemName, Object value) {
    try {
        if (value == null) {
             doc.removeItem(itemName);
         }
        doc.replaceItemValue(itemName, value);
    } catch (NotesException ne) {
        handleException(ne);
    }
    changed = true;
}
public void close() {
    if (doc == null)
        return;
    try {
        if (changed == true) {
            doc.save();
        }
        doc.recycle();
        doc = null;
    } catch (NotesException ne) {
        handleException(ne);
    }
}
...
protected boolean delete() throws DeleteException, InvalidStateException {
    this.checkState();
    boolean ret = false;
    try {
        synchronized (this) {
        ret = doc.remove(true);
        doc = null;
    }
    } catch(NotesException ne) {
        handleException(ne);
    }
    return ret; 
}
protected void checkState() throws InvalidStateException {
    if(doc == null) throw 
        new InvalidStateException("Business Object with id 
        ["+this.getId()+"] is in an invalid state");
    }
}

客户类

以下代码示例展示了客户类的一部分,该类包含查找一个或所有相关订单、创建新的订单、通过 set 和 get 方法访问该客户的属性以及删除该客户文档自身的方法。像表示存储在 Notes 文档中的信息的每个业务对象类那样,客户类扩展了 BOEntity 类。派生业务对象实体类的所有构造函数都受到保护,所以,业务对象包以外的每个代码必须使用父类的适当 create 方法来实例化业务对象实体类。

在每个可以包含从属业务对象的业务对象类中,都有一类重要的方法,即一组用于检索这些从属对象的 find 方法。在客户类中,提供了两个不同的 find 方法。第一个 find 方法称为 findOrderById(),它构建了一个密钥,该密钥包含搜索到的对象类型、实例的客户 ID 和传递的订单 ID。这个密钥被传递给从 BOBase 类(参见前面的代码示例)中继承的一般的 findById() 方法。该方法返回的 Notes 文档打包在订单类的一个实例中。客户类中的第二个 find 方法是 findAllOrder(),它使用了一个包含搜索到的对象类型和该实例的客户 ID 的密钥。这个密钥被传递给从 BOBase 类中继承的一般的 findAll() 方法。返回的 Notes 文档集合的每一个 Notes 文档都被打包在它们自己的订单类的实例中。findAllOrder() 方法在一个向量实例中返回所有订单实例。

package acme;
import lotus.domino.*;
import java.util.Vector;
public class Customer extends BOEntity {
 ...
public Order findOrderById(String orderId) throws InvalidIdException {
    Key key = new Key();
    key.appendEntry(Order.NAME);
    key.appendEntry(this.getId());
    key.appendEntry(orderId);
    Document doc = findByID(key);
    return new Order(orderId, this.getId(), doc, getBOSession());
}
public Vector findAllOrder() {
    Key key = new Key();
    key.appendEntry(Order.NAME);
    key.appendEntry(this.getId());
    DocumentCollection documents = findAll(key);
    Vector vecOrders = new Vector();
    try {
        if(documents != null && documents.getCount() > 0) {
            Document doc = documents.getFirstDocument();
            while (doc != null) {
                Order order = new Order(doc.getItemValueString(Order.ITEM_OBJECT_ID),
                    this.getId(), doc, getBOSession());
                vecOrders.addElement(order); 
                doc = documents.getNextDocument();
            }
        } 
    } catch (NotesException ne) {
        handleException(ne);
    }
    return vecOrders;
}
...
public String getLastName() {
    return readString(ITEM_NAME_LASTNAME);
}
public void setLastName(String lastName) {
    writeValue(ITEM_NAME_LASTNAME,lastName);
}
public Order createOrder(String newOrderId) throws CreateException, 
InvalidStateException {
this.checkState();
    synchronized (this.getClass()) {
    try {
        Order order = this.findOrderById(newOrderId);
        if(order != null) {
            order.close();
            throw new DuplicateIdException("Order with Id 
            "+newOrderId+" already exists.");
        }
    } catch (InvalidIdException e) {
    // ignore exception if an order with the new id doesn't exist
    }
    Document doc = createEntity(newOrderId, getParentId(),Order.NAME);
    return new Order(newOrderId, getParentId(), doc, getBOSession());
    }
}
public boolean delete() throws DeleteException, InvalidStateException {
    long n = getOrderCount();
    if(n != 0) {
        // optionally check the status of the order
        throw new DeleteException("Can't delete customer ["+this.getId()+"], 
        because associated orders exist.");
    }
    // ... or alternative implementation: delete all dependent objects
    return super.delete();
}

createOrder() 方法

前面的代码示例中的方法 createOrder() 使用客户类的 findOrderById() 方法,来确定包含为该客户传递的 ID 的订单是否已经存在于 Notes 数据库中。如果已经存在这样一个订单,则抛出适当的 DuplicateIdException,并取消创建该订单。如果不存在包含传递的 ID 的订单,则抛出的 InvalidIdException 被忽略,并使用 createEntity() 方法为该客户创建保存订单信息的新文档。返回的新文档将包含父(客户)ID 及其自己的订单 ID。此外,表单项将指定这类订单的文档。新文档被传递给该订单类的客户。而这个业务对象在从 BOEntity 类继承的一个私有实例变量中封装 Notes 文档,这样可以保护任何对该文档及其项的直接访问。只有订单类的 set 和 get 方法允许访问该业务对象的信息。

方法 createOrder() 包含前面用同步的代码块描述的代码。同步的代码块在执行之前需要与客户类对象相关的锁。该代码保证,代码序列在同一时间只能由客户类的一个实例在 Java Virtual Machine 中的一个线程中执行,而不管有多少线程试图执行不同客户实例的 createOrder() 方法的代码。

前面代码示例中的 delete 方法展示了这些类如何有助于确保业务对象的一致性。在这个业务示例中,只要存在相关的订单,就不允许删除客户。

利用 checkState() 方法调用作为 BOBase 类的 delete 方法中的守卫条件 ,在相同的代码序列中通过 BOSession 类的适当 findCustomerById() 方法来检索客户实例、删除该客户以及为该客户创建新的订单而不会强行抛出一个异常是不可能的,如下面的代码示例所示。

Customer customer = boSession.createCustomer("C9999");
// further processing ...
customer.delete(); 
// further processing ...
// this customer instance isn't valid anymore,
// therefore the createOrder() will throw a InvalidStateException
customer.createOrder("22");
customer.close();

该代码序列导致了一个 InvalidStateException。

acme.InvalidStateException: Business Object with id [C9999] is in an invalid state
    at acme.BOEntity.checkState(BOEntity.java:165)    
    at acme.Customer.createOrder(Customer.java:79)    
    at Example6.main(Example6.java:26)

您需要了解的事

应该说明的是,本文中的示例有点简单。以下列表概括了一些主题,并略述了该示例的一些可能增强:

  • 只有一个 Notes 数据库被认为是相关业务对象的源数据库。
    另外,通过覆盖 BOBase 的 getDatabase() 方法以及已覆盖的 getLupView() 方法返回的适当相关视图,每个派生的业务对象实体类都可以拥有关于其源 Notes 数据库的信息。
  • 只有一个 Notes 查找视图被 find 方法使用。
    在更复杂的系统中,可能需要更专门的查找视图和相应的 find 方法。比如说,可以使用 findCustomerByName 方法通过客户的姓氏来搜索客户,从而实现一般用途的用例场景。
  • 在该例中,所有信息都是通过访问适当的文档来检索的。
    例如,一些特殊的只读业务对象可以控制对 Notes View Entry Collection 提供的信息的访问和封装,返回一个来自概述列表的客户概要信息的大型集合,而不是使用 findAllCustomer 方法,返回已封装客户文档的集合。
  • 您可以使用 Notes 6 或 Domino 6 基础设施上下文中的 Java 2 collection API 接口和类,而不是使用该例中使用的向量类。
    Java 1.1 类的层次结构只包含对容器和抽象数据类型的类的基本支持;Java 2 引进了 Java Collection API 来弥补 Java 1.1 API 中的这个缺憾。
  • 客户类可以是用于许多不同客户类型的抽象基类,比如说私有客户或业务客户。
    findAllCustomer 方法可以返回不同派生客户类型实例的一个向量,而专门的 find 方法可以返回适当的子集。
  • 在该例中,业务对象之间不存在程序化的关联。
    您可以在适当客户的实例变量中存储订单集合。
  • 在该例中,BOSession 被用在一个单独的程序上下文中,该上下文中包含一个匿名 Notes 会话。
    不过,可以用 servlet 或 EJB 上下文中的 LTPA 令牌来实例化它。
  • 如果所有创建操作都在同一 Java 虚拟机(JVM)中执行,那么所展示的防止 create 方法中出现包含已同步代码的重复 ID 的方法将工作得很好。
    如在不同的 JVM 中执行程序(比如,使用 Web-triggered Domino Java Agents 或两个不同 WAS 实例中的 BO 类),那么所描述的创建惟一 ID 的机制将不起作用!解决这个问题的方法可能是通过一个“集中式”servlet 引擎中的某个 servlet 的 HTTP,使用适当的调用替换 create 方法中的已同步代码。这个集中式 servlet 引擎应该利用适当的故障恢复机制提供高可用性。不过,这个主题超出了本文的介绍范围。
  • 如果只有业务对象代码直接访问适当的 Notes 文档,那么只能执行保证业务对象一致性的语句。
  • 在该例中,因为两个业务对象之间具有从属性,所以在访问相关的订单之前,有必要获得一个客户实例。
    该容器层次结构并不存在于每个业务域上下文中。所有业务对象都可以通过它们自己的惟一密钥而不是层次结构密钥来访问。

结束语

因为更多地重用代码,所以本文中介绍的使用业务域驱动的类层次结构的概念可以简化 Domino 应用程序的开发。在一个小型的公共基类集合中封装特定于 Domino 的实现细节可以减少对适当的 Domino Java API 技能的需要,并且支持创建无错误且易于维护的代码。

致谢

感谢 Ragnar Schierholz,Kai-Hendrik 去年考虑让他作为自己的考生,现在,Ragnar Schierholz 已经是圣加龙大学的一名博士应考生,感谢他对本文概念所做的一些有益讨论。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Lotus
ArticleID=49274
ArticleTitle=Domino 的业务域驱动 Java 类的层次结构
publish-date=12232004