上一级 首页 下一级

详细设计和实现

确定了本项目的整体架构以后,可以针对本项目所要求的具体功能,进行具体详细的设计和实现。

J2EE项目开发的第一个突破口一般是在业务对象建模,有了系统的基本对象后才可以进行数据库设计,进而再实现系统的逻辑处理,最后实现表现层。这就是所谓“Domain FirstPersistence SecondServices ThirdClients Last”。

3.1  域模型设计

从系统需求来看,一个页面划分为页面布局、标题栏、页尾栏、菜单栏和内容栏部分,其中页面布局以及标题栏和页尾栏属于页面模板部分,可以由JSP借助Tiel来实现,每个页面变动的就是菜单栏和内容栏。因此,菜单栏和内容栏是本项目的主要处理对象,可以建立相关基本对象,如图4-6所示。

4-6  系统的基本对象

Body对象是代表页面的内容部分,而Menu是代表菜单。由于一个页面可能有很多菜单,同时可能还有指向上一级页面的菜单,许多菜单组合在一起,可以使用Navlink对象来代表。

对象的创建顺序是:先创建菜单对象Menu,然后是Body对象,最后是模板合成生成页面,其中MenuBody对象的创建都涉及到数据层的操作。对象创建流程如图4-7所示。

4-7  创建流程

3.2  域模型的实现

模型(Model)的实现类似原来传统的数据库数据表的设计。在面向对象系统分析设计中,常用域模型设计替代以往的数据表结构设计。在本项目中,由于采取数据对象持久化到XML的设计方案,因此,只要设计实现数据对象就可以,如图4-8所示。

4-8  数据模型的实现

MenuModel中有5个属性,分别是:

·          Id:菜单的Id

·          Name:菜单的名称,比如“关于我们”或“产品服务”。

·          Icon:菜单的图标,有时菜单可能不直接是菜单名的文本,而是特定图标。

·          Link:菜单指向的页面,是一个JSP页面。

·          Datalink:菜单指向的内容。

下一步根据Castor XML要求,需要设计数据对象和XML文本结构的映射,以MenuNavlink为例:

<?xml version="1.0" encoding="UTF-8"?>

<mapping>

  <description>a map file for our new template system</description>

   <class name="com.jdon.cms.model.MenuModel">

        <map-to xml="menu"/>

        <field name="id" type="integer">

           <!-- 表明idXML元素的一个属性   -->

          <bind-xml name="id" node="attribute" />

        </field>

         <!-- 对应MenuModel对象中的name属性  -->

        <field name="name" type="string" />

         <!-- 对应MenuModel对象中的icon属性  -->

        <field name="icon" type="string" />

         <!-- 对应MenuModel对象中的link属性  -->

        <field name="link" type="string" />

         <!-- 对应MenuModel对象中的datalink属性  -->

        <field name="dataLink" type="string" />

    </class>

 

    <class name="com.jdon.cms.model.NavlinkModel">

        <map-to xml="navlink"/>

        <field name="id" type="integer" />

        <field name="count" type="integer" />

    <!-- 表明这个Navlink中包含menu节点集合   -->

        <field name="menus"

type="com.jdon.cms.model.MenuModel" collection="collection" >

          <xml name="menu" node="element" />

        </field>

    </class>

</mapping>

通过调用CastorHandler可以实现MenuModel对象到XML文件的序列化,其他数据对象也如此。

由此,数据对象的实现基本完成。当然,数据对象建模不是一次性就能成功,可能要经过反复斟酌修改。数据对象建模可以反映设计者对应用系统的真正认知能力,因此也是非常重要的一个步骤。

下一步将进入逻辑处理层的具体实现。逻辑处理层是本项目的核心功能层,设计方式将采取模式驱动设计(Pattern Driven Design)的方式。

3.3  抽象工厂模式

在本项目中,使用了XML文件系统作为数据存储介质,但是如果将来系统发展到一定规模,使用关系数据库作为存储介质,那么就需要替换存储介质。这种替换工作力求修改最小,不影响原有系统的稳定性,最好能无缝过渡到数据库系统,也就是说为了让系统具备灵活的扩展性和伸缩性。

为了达到这个目的,需要将有关XML数据操作的细节封装起来,使得外界根本不知道系统内部是采取XML文件还是使用数据库来实现数据持久化的。

还有一个目的,目前本项目没有加入用户权限控制,只有一个角色“管理员”可以操作数据。如果将来需要增加新的角色,这个角色只有修改页面内容的权限,没有创建新页面或删除页面的权限。这就需要在访问数据层之间加一个网关Proxy,用来进行权限访问控制。

抽象工厂模式可以满足这些要求,抽象工厂主要是提供创建系列对象的接口,使得外界无需知道创建这系列对象的具体细节。

实现抽象工厂,需要有下面4个参与者:

·          抽象工厂:主要是提供创建对象的接口,外界直接和这个抽象工厂交互。

·          抽象工厂的具体实现者: 主要是实现创建对象的具体细节,将有关XML文件具体实现封装在这个类中,将来可以再做一个使用关系数据的具体实现者。

·          抽象产品:产品的抽象接口。

·          抽象产品的具体实现者:详细定义上述抽象产品的细节。

下面将分别编写上述几个参与者的接口或类。首先,定义一个抽象工厂类PageFactory

/**

 *  Page工厂

 * <p>Copyright: Jdon.com Copyright (c) 2003</p>

 * <p>Company: 上海汲道计算机技术有限公司</p>

 * @author banq

 * @version 1.0

 */

public abstract class PageFactory {

    private static Object initLock = new Object();

     //定义抽象工厂的具体实现类的名称

    private static String className =

        "com.jdon.cms.xml.XmlPageFactory";

    private static PageFactory factory = null;

 

    //Singleton模式建立统一调用接口

    public static PageFactory getInstance() {

      if (factory == null) {

        synchronized (initLock) {

          if (factory == null) {

            try {

              //使用动态类装载机制将抽象工厂具体实现者激活

              Class c = Class.forName(className);

              //类似 PageFactory pageFactory = new XmlPageFactory

              factory = (PageFactory) c.newInstance();

            }

            catch (Exception e) {

              Debug.logError(" get factory instance error:" + e, module);

              return null;

            }

          }

        }

      }

      //其实是返回 XmlPageFactory的实例

      return factory;

    }

 

//获得导航条对象

    public abstract Navlink getNavlink();

    //创建新的菜单

    public abstract Menu createMenu(Integer Id);

    //创建新的内容

   public abstract Body createBody(Integer Id);

    …

}

上面PageFactory类中动态指定了XmlPageFactory作为PageFactory的具体实现者,XmlPageFactory中封装的是数据持久化保存到XML文件系统中的操作过程。如果需要使用数据库系统,那么只要制作一个DatabasePageFactory,将上面程序中的className的值改为 DatabasePageFactory,那么整个系统的存储介质就转换到数据库系统上了。

另外,如果需要访问权限控制,可以创建一个抽象工厂的实现者如PageFactoryProxy,在PageFactoryProxy中对每个数据操作方法都进行一次权限是否允许的检验。

抽象工厂模式实际上是实现了数据层和逻辑层的分离,使得架构设计中的多层概念能够得以真正实现。架构设计中多层分离不能只是一种设计预想,还必须依靠一定的手段去真正落实,而设计模式恰好是有力的实现手段,这也就是所谓Pattern Driven Architecture

使用模式的目的是增强系统的可重用性,降低耦合性,提高灵活性,掌握这个原则才真正学会使用模式。下面的分析将自然导出委托模式的使用。

继续分析XmlPageFactory类,在这个类中需要实现有关Menu对象和Body对象的XML操作。这就会出现一个问题,在这个类中将可能混合了很多功能,随着系统的扩展,功能的增多,会使得这个类变得庞乱复杂,增加了维护的困难性。

细化是面向对象设计自始至终的目标。具体原则就是:封装和分派。以被操作的对象为归类,将有关Menu对象和Body对象的功能分别委托另外的类来实现,这样XmlPageFactory类就会干净多了,而且如果有关Menu对象再进行修改,将不会直接修改XmlPageFactory这个关键的类,只会去修改委托类。比如是NavlinkManager,降低了修改工作带来的对系统稳定性的冲击。

同样有关Body对象的功能操作,则可以委托给BodyManager来实现,如图4-9所示。

4-9  抽象工厂的流程图

XmlPageFactory的代码如下:

public class XmlPageFactory extends PageFactory {

  public final static String module = XmlPageFactory.class.getName();

  private final static XmlUtil xmlUtil = XmlUtil.getInstance();

  //初始化Cache

  public static UtilCache contentCache = new UtilCache(5, 0);

  public static UtilCache navlinkCache = new UtilCache();

  public static UtilCache bodyCache = new UtilCache();

  //初始化被委托者

  private NavlinkManager navlinkManager = null;

  private BodyManager bodyManager = null;

  private ContentManager contentManager = null;

  private ContentFilter contentFilter = null;

 

  public XmlPageFactory() {

    navlinkManager = new NavlinkManager();

    bodyManager = new BodyManager();

    contentManager = new ContentManager();

    contentFilter = new HTMLFilter();

  }

 

  public Navlink getNavlink() {

    NavlinkModel navlink = (NavlinkModel) navlinkCache.get(Navlink.NAME);

    if ( (navlink == null) || (navlink.isModified())) {

      navlink = navlinkManager.getNavlinkFromFile();

      navlinkCache.put(Navlink.NAME, navlink);

    }

    return navlink;

  }

 

  public Integer getId() {

    Integer newId = null;

    Navlink navlink = getNavlink();

    if (navlink.getCount() == null)

        newId = new Integer(1);

    else{

      newId = new Integer(navlink.getCount().intValue() + 1);

    }

    navlink.setCount(newId);

    return newId;

  }

 

  public Menu createMenu(Integer Id) {

    return navlinkManager.createMenu(Id);

  }

 

  public Menu getMenu(Integer Id, Navlink navlink) {

    Menu menu = null;

    try {

      menu = navlinkManager.findByPrimaryKey(Id, navlink);

    } catch (Exception ex) {

      Debug.logError(" getMenu error:" + ex, module);

    }

    return menu;

  }

 

  public Menu updateMenu(Menu menu, Page page) {

    return navlinkManager.updateMenu(menu, page);

  }

 

  public void deleteMenu(Menu menu, Navlink navlink) {

    navlinkManager.deleteMenu(menu, navlink);

  }

  …

}

至此,在逻辑处理层中使用了抽象工厂模式。这样既能实现系统架构的层次分离,又能实现系统的动态扩展性,为以后扩展到数据库系统做好了准备,将相关代码改动所带来的损失降低到了最低程度,保证了系统的稳定性。


3.4  生成器(Builder)模式

本项目关键功能是将页面内容和模板有机地混合在一起,这个混合过程实际是一种组装创建的过程,依据不同的模板技术会有不同的生成组装过程。

本项目设计中采取的是Tile模板技术,一个页面由几个区域的JSP文件组成,其中关键部分有两点:

·          将菜单(Menu)数据和菜单显示JSP 结合在一起。

·          将内容(Body)数据和内容显示JSP结合在一起。

这种具体实现细节有很多种办法,每个办法都有一定的优点和缺点,其中有一个最容易实现的方案:

·          在菜单JSP中动态访问Navlink中的Menu,将Menu对象遍历后输出。

·          在内容JSP中读取BodyXml数据,由于内容部分可能包含大量HTML,而且数据可能庞大,再采取一个简单变通的办法,直接将这些包含HTML的内容存储为内容JSP文件,那么原来Body对象实际变成了有关内容属性的对象。

上述方案只是众多方案中的一种,假设该方案叫Standard方案,如果以后有更好的算法方案,应该能方便地替换Standard方案,这时就需要有一个可替换的机制。将Standard方案封装起来就可以实现替换,生成器Builder模式能够实现这种组装过程的封装。

Builder模式是一种将复杂对象的组装创建过程封装起来的模式,将过程中具体实现细节隐含在抽象接口后面。想到Builder模式应该就会联想到汽车的组装:汽车分很多部件,部件组装起来就成为一辆汽车,Builder模式是将汽车组装过程进行了封装。

“页”是这个项目中的一个复杂对象,需要通过复杂的混合组装过程,将内容在一个“页”中以一定模板显示出来。分别依据Builder模式的参与者设计如下:

“页(Page)” Builder模式中要构造的复杂对象。“页(Page)”是由菜单(Menu)、内容(body)以及模板JSP等部件组成。

Builder模式中生成器是部件创建的抽象接口,在这个接口中,定义部件创建的一些方法:

/**

 * Builder模式接口

 * <p>Copyright: Jdon.com Copyright (c) 2003</p>

 * <p>Company: 上海汲道计算机技术有限公司</p>

 * @author banq

 * @version 1.0

 */

public interface PageBuilder {

    //创建Menu部件的方法

    public Menu buildMenu(Integer Id);

    //创建Content部件的方法

    public String buildContent(Integer Id);

    //创建Body部件的方法

    public Body buildBody(Integer Id);

    //创建模板JSP部件的方法

    public boolean buildTemplate(Integer Id);

    //获得组装构建后的产品:“页(Page)”

    public Page getPage();

}

在对“页(Page)”有了准确的部件分解后,才会有PageBuilder中抽象的部件构建方法。不管以后如何变化,Page组成的部件是不会变化的。正是有这个不变的前提,才能封装部件的创建以及部件之间组合的过程。

Builder模式中的Director 是指导部件的组装,实现与外界的接口:

/**

 * Builder模式Director

 * <p>Copyright: Jdon.com Copyright (c) 2003</p>

 * <p>Company: 上海汲道计算机技术有限公司</p>

 * @author banq

 * @version 1.0

 */

public class PageRender {

 

  private PageBuilder pageBuilder = null;

  //构造PageRender类时需要指定一个生成器

  public PageRender(PageBuilder pageBuilder){

    this.pageBuilder = pageBuilder;

  }

 

  //创建组构造页面,该方法是被外界访问和调用的

  public boolean construct(Integer Id) {

     //执行菜单Menu组装

     Menu menu = pageBuilder.buildMenu(Id);

     //执行Content组装

     String output = pageBuilder.buildContent(Id);

     //执行Body组装

     Body body = pageBuilder.buildBody(Id);

     //执行模板组装

     if (pageBuilder.buildTemplate(Id)){

       Debug.logVerbose(" -->>>finish constructing a new page", module);

       return true;

     }else{

       return false;

     }

 

  }

 

   //解构页面方法,如果页面需要被删除

   //需要逐步删除或撤除这些部件

   public void unConstruct(Integer Id) {

      //首先删除模板 这与创建时顺序相反

      pageBuilder.buildTemplate(Id);

      //删除Content

      pageBuilder.buildContent(Id);

      //删除Body

      pageBuilder.buildBody(Id);

      //最后删除菜单

      pageBuilder.buildMenu(Id);

   }

}

上例中,已经看到Director如何指挥一个Page产品的组装和拆除过程。组装的过程是有先后次序的:菜单总是需要第一个被创建,有了菜单,才有菜单指向的内容,内容有了,才会有页面的模板显示。而删除页面时,整个次序则倒过来,从外向里逐个剥离开。

设计好3个主要的参与者,Builder模式基本确立,还有一个是参与者是生成器(Builder)的具体实现者。

StandardPageBuilderPageBuilder的具体实现者。在这个类中,封装的是Standard方案。如果将来有其他更好的实现细节,可以再创建一个类。比如BestPageBuilder,使用BestPageBuilder代替StandardPageBuilder就可以使系统有一个更好的算法,而这种切换带来的修改是非常小的。从外界调用的变化中可以看出:

采取Standard方案时外界的调用如下:

  PageBuilder pageBuilder = new StandardPageBuilder(pageEvent);

  PageRender render = new PageRender(pageBuilder);

  render.construct(Id);

采取Best方案后的调用如下:

  PageBuilder pageBuilder = new BestPageuilder (pageEvent);

  PageRender render = new PageRender(pageBuilder);

  render.construct(Id);

可见变化的只是一个实例名的更改。

StandardPageBuilder代码的主要部分如下:

public class StandardPageBuilder implements PageBuilder {

  private final static String module = StandardPageBuilder.class.getName();

  private final static XmlUtil xmlUtil = XmlUtil.getInstance();

  //初始化PageFactory

  private final static PageFactory pageFactory = PageFactory.getInstance();

 

  private Page page = null;

  private String action = null;

  public StandardPageBuilder(PageEvent pageEvent) {

    this.page = pageEvent.getPage();

    this.action = pageEvent.getAction();

  }

   /**

   * 生成Menu

   */

  public Menu buildMenu(Integer Id) {

    if (action.equals(PageEvent.CREATE))

      return createMenu(Id);

    else if (action.equals(PageEvent.QUERY))

      return queryMenu(Id);

    else if (action.equals(PageEvent.EDIT))

      return updateMenu(Id);

    else if (action.equals(PageEvent.DELETE))

      deleteMenu(Id);

    return null;

  }

  /**

   * 1. 创建新的Menu实例

   * 2. 保存到持久层

   */

  private Menu createMenu(Integer Id) {

    Debug.logVerbose(" -->> enter build menu:" + page.getName(), module);

    Menu menu = pageFactory.createMenu(Id);

    pageFactory.updateMenu(menu, page);

    Debug.logVerbose("menu object created, ID is " + menu.getId(), module);

    Navlink navlink = page.getNavlink();

    navlink.setId(Id);

    //add the menu to the navlink

    navlink.addMenu(menu);

    try {

      //委托pageFactory实现存储操作

      pageFactory.saveNavlink(navlink);

      Debug.logVerbose(" --> >build Menu ok", module);

      page.setNavlink(navlink);

    } catch (Exception ex) {

      Debug.logError(" build Menu error:" + ex, module);

      menu = null;

    }

    return menu;

  }

  …

  //生成Body

  public Body buildBody(Integer Id) {

    if (action.equals(PageEvent.CREATE))

      return createBody(Id);

    else if (action.equals(PageEvent.QUERY))

      return queryBody(Id);

    else if (action.equals(PageEvent.EDIT))

      return updateBody(Id);

    else if (action.equals(PageEvent.DELETE))

      deleteBody(Id);

    return null;

  }

  //委托PageFactory实现Body数据的创建

  private Body createBody(Integer Id) {

    Debug.logVerbose(" -->> enter build Body " + page.getTitle(), module);

    String author = "";

    String title = page.getTitle();

    //body's content is the file name that contains the content of the page

    String content = page.getHtmlText();

    Integer hits = new Integer(0);

    Integer status = new Integer(0);

    Body body = null;

    try {

      body = pageFactory.createBody(Id);

      body.setAuthor(author);

      body.setTitle(title);

      body.setContent(content);

      body.setHits(hits);

      body.setStatus(status);

      pageFactory.updateBody(body);

    } catch (Exception e) {

      Debug.logError(" build body error in :" + e);

    }

    return body;

  }

 …

 //生成模板

 public boolean buildTemplate(Integer Id) {

    if (action.equals(PageEvent.CREATE))

      return createTemplate(Id);

    else if (action.equals(PageEvent.EDIT))

      return createTemplate(Id);

    else if (action.equals(PageEvent.DELETE))

      deleteTemplate(Id);

    return true;

  }

  //创建模板文件xxx.jsp

  public boolean createTemplate(Integer Id) {

    boolean success = false;

    Menu menu = null;

    try {

      Debug.logVerbose("--> find the menu by the Id: " + Id, module);

      //PageFactory获取Menu对象

      menu = pageFactory.getMenu(Id, page.getNavlink());

      if (menu == null)

        return success;

      String titleValue = menu.getName();

      String bodyValue = menu.getDataLink();

      Debug.logVerbose("--> jspFileName =" + menu.getLink() + " bodyValue=" +

                       bodyValue, module);

     

      String jspPathName = XmlUtil.getJspDirStr(menu.getId().toString());

      //jsp文件操作实际委托JspMaker操作

      success = JspMaker.generate(jspPathName, titleValue, bodyValue);

    } catch (Exception ex) {

      Debug.logError(" nof get the menu id =" + Id, module);

      return success;

    }

    return success;

  }

  …

}

在图4-10中,PageHandler作为Builder模式的客户端,通过使用PageRenderconstruct访问来构建页面产品。而StandardPageBuilder作为Builder接口的一个实现,它又将有关Menu Body等对象的操作委托给PageFactory来完成。

结合图4-10和图4-9,本项目的整个流程控制图基本已经明晰,用户向表现层发出请求,在表现层封装成基本对象后,交由图4-10中的PageHandler处理,而PageHandler将复杂的组装过程委托给Builder模式来实现。在采取Standard方案组装“页(page)”的StandardPageBuilder中,凡是涉及到数据层操作,例如数据持久化保存等,都再次委托给PageFactory去处理。

4-10  Builder模式的流程图

通过图4-9PageFactory封装了数据层的操作细节其中一个是使用XML文件作为数据保存的方案XmlPageFactoryXmlPageFactory又将各个部件对象的数据操作委托NavlinkManagerBodyManager这样的类来实现。在NavlinkManager中调用DBO模式的实现者CastorHandler,而在CastorHandler中封装的是数据对象序列化或反序列化的细节和相关XML API操作细节。

本项目的整体设计主要体现了模式驱动的架构设计,在整个流程图中,每个模式很好地实现和封装了项目需求的各个功能,而且每个模式又层层相扣且紧密衔接在一起,就如同模板有机地拼装在一起。更重要的是,这样一个系统是一个具有很强伸缩性的动态可扩展的、稳健的系统,而且便于他人阅读理解。

假设不采取这样的模式驱动思路来设计,这个系统将会怎样?首先,多层架构分离就无法很好地实现,这样,数据持久化操作也许可能就在表现层中完成;其次,菜单或内容对象的操作以及组装细节可能混合充斥在一两个类中实现,按照这样的过程化设计思路编制后的系统将是怎样?也许可以运行,也许调试时就会出现错误无法定位的问题,难于调试,更加难于维护和拓展。

更有甚者,如果有一天需要使用数据库来实现菜单或内容对象的数据持久化,整个系统就会重建。重建的代价实际是破坏了系统的稳定性,客户的抱怨会严重影响公司的声誉。 

所有这些代价的付出都是由于设计的简单性,其中痛苦和无奈是很多有经验的程序员经常碰到的。



上一级 首页 下一级