每日一得

不求多得,只求一得 about java,hibernate,spring,design,database,Ror,ruby,快速开发
最近关心的内容:SSH,seam,flex,敏捷,TDD
本站的官方站点是:颠覆软件

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  220 随笔 :: 9 文章 :: 421 评论 :: 0 Trackbacks

2  Jive 与设计模式

Jive 论坛系统使用大量设计模式巧妙地实现了一系列功能。因为设计模式的通用性和可理解性,将帮助更多人很快地理解 Jive 论坛源码,从而可以依据一种“协定”来动态地扩展它。那么使用设计模式还有哪些好处?

2.1  设计模式

设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。毫无疑问,设计模式于己于他人于系统都是多赢的。设计模式使代码编制真正工程化,设计模式是软件工程的基石。

GOF (设计模式作者简称)《设计模式》这本书第一次将设计模式提升到理论高度,并将之规范化,该书提出了 23 种基本设计模式。自此,在可复用面向对象软件的发展过程中,新的大量的设计模式不断出现。

很多人都知道 Java 是完全面向对象的设计和编程语言,但是由于接受教育以及经验的原因,大多数程序员或设计人员都是从传统的过程语言转变而来,因此在思维习惯上要完全转变为面向对象的设计和开发方式是困难的,而学习设计模式可以更好地帮助和坚固这种转变。

凡是学习完成设计模式的人都有一种类似重生的感觉,这种重生可以从很多方面去解释。换一种新的角度来看待和解决问题应该是一种比较贴切的解释,而这种新的思维角度培养属于基础培训,因此,设计模式是学习 Java 的必读基础课程之一。

由于设计模式概念比较抽象,对于初学者学习有一定的难度,因此结合 Jive 论坛系统学习设计模式将是一种很好的选择。

掌握了设计模式,将会帮助程序员或设计人员以更加可重用性、可伸缩性的眼光来开发应用系统,甚至开发通用的框架系统。框架系统是构成一类特定软件可复用设计的一组相互协作的类,主要是对应用系统中反复重用部分的提炼,类似一种模板,这是一种结构性的模板。

框架通常定义了应用体系的整体结构、类和对象的关系等设计参数,以便于具体应用实现者能集中精力于应用本身的特定细节。框架强调设计复用,而设计模式最小的可重用单位,因此框架不可避免地会反复使用到设计模式。关于通用框架系统的设计开发将在以后章节中讨论。

其实 Jive 论坛本身也形成了一个基于 Web 结构的通用框架系统,因为它很多设计思想是可以重用的,例如设定一个总体入口,通过入口检查用户的访问控制权限,当然还有其他各方面的功能实现方式都是值得在其他系统中借鉴的,也正因为它以模式的形式表现出来,这种可重用性和可借鉴性就更强。

2.2  ForumFactory 与工厂模式

工厂模式是 GOF 设计模式的主要常用模式,它主要是为创建对象提供了一种接口,工厂模式主要是封装了创建对象的细节过程,从而使得外界调用一个对象时,根本无需关心这个对象是如何产生的。

GOF 设计模式中,工厂模式分为工厂方法模式和抽象工厂模式。两者主要区别是,工厂方法是创建一种产品接口下的产品对象,而抽象工厂模式是创建多种产品接口下的产品对象,非常类似 Builder 生成器模式。在平时实践中,使用较多的基本是工厂方法模式。

以类 SampleOne 为例,要创建 SampleOne 的对象实例 :

SampleOne sampleOne = new SampleOne();

如果 Sample 类有几个相近的类: SampleTwo SampleThree ,那么创建它们的实例分别是:

SampleTwo sampleTwo = new SampleTwo();

SampleThree sampleThree = new SampleThree();

其实这 3 个类都有一些共同的特征,如网上商店中销售书籍、玩具或者化妆品。虽然它们是不同的具体产品,但是它们有一个共同特征,可以抽象为“商品”。日常生活中很多东西都可以这样高度抽象成一种接口形式。上面这 3 个类如果可以抽象为一个统一接口 SampleIF ,那么上面语句就可以成为:

SampleIF sampleOne = new SampleOne();

SampleIF sampleTwo = new SampleTwo();

SampleIF sampleThree = new SampleThree();

在实际情况中,有时并不需要同时生成 3 种对象,而是根据情况在 3 者之中选一个。在这种情况下,需要使用工厂方法来完成了,创建一个叫 SampleFactory 的抽象类:

public class SampleFactory{

   public abstract SampleIF creator();

}

在这个抽象工厂类中有一个抽象方法 creator ,但是没有具体实现,而是延迟到它的子类中实现,创建子类 SampleFactoryImp

public class SampleFactoryImp extends SampleFactory{

   public SampleIF creator(){

    // 根据其他因素综合判断返回具体产品

    // 假设应该返回 SampleOne 对象

       return new SampleOne();

}

}

SampleFactoryImp 中根据具体情况来选择返回 SampleOne SampleTwo SampleThree 。所谓具体情况有很多种:上下文其他过程计算结果;直接根据配置文件中配置。

上述工厂方法模式中涉及到一个抽象产品接口 Sample ,如果还有其他完全不同的产品接口,如 Product 等,一个子类 SampleFactoryImp 只能实现一套系列产品方案的生产,如果还需要另外一套系统产品方案,就可能需要另外一个子类 SampleFactoryImpTwo 来实现。这样,多个产品系列、多个工厂方法就形成了抽象工厂模式。

前面已经讨论在 Jive 中设置了论坛统一入口,这个统一入口就是 ForumFactory ,以下是 ForumFactory 的主要代码:

public abstract class ForumFactory {

   private static Object initLock = new Object();

   private static String className = " com.Yasna.forum.database.DbForumFactory";

   private static ForumFactory factory = null;

  

   public static ForumFactory getInstance(Authorization authorization) {

     if (authorization == null) {

       return null;

     }

     // 以下使用了 Singleton 单态模式,将在 2.3 节讨论

     if (factory == null) {

       synchronized(initLock) {

         if (factory == null) {

             ... // 从配置文件中获得当前 className

           try {

               // 动态装载类

               Class c = Class.forName(className);

               factory = (ForumFactory)c.newInstance();

           }

           catch (Exception e) {

               return null;

           }

         }

       }

     }

     // 返回 proxy. 用来限制授权对 forum 的访问

     return new ForumFactoryProxy(authorization, factory,factory.getPermissions(authorization));

   }

   // 创键产品接口 Forum 的具体对象实例

   public abstract Forum createForum(String name, String description)

   throws UnauthorizedException, ForumAlreadyExistsException;

   // 创键产品接口 ForumThread 的具体对象实例

public abstract ForumThread createThread(ForumMessage rootMessage)

throws UnauthorizedException;

// 创键产品接口 ForumMessage 的具体对象实例

 

 

    public abstract ForumMessage createMessage();

   ....

}

ForumFactory 中提供了很多抽象方法如 createForum createThread createMessage() 等,它们是创建各自产品接口下的具体对象,这 3 个接口就是前面分析的基本业务对象 Forum ForumThread ForumMessage ,这些创建方法在 ForumFactory 中却不立即执行,而是推迟到 ForumFactory 子类中实现。

ForumFactory 的子类实现是 com.Yasna.forum.database.DbForumFactory ,这是一种数据库实现方式。即在 DbForumFactory 中分别实现了在数据库中 createForum createThread createMessage() 3 种方法,当然也提供了动态扩展到另外一套系列产品的生产方案的可能。如果使用 XML 来实现,那么可以编制一个 XmlForumFactory 的具体工厂子类来分别实现 3 种创建方法。

因此, Jive 论坛在统一入口处使用了抽象工厂模式来动态地创建论坛中所需要的各种产品,如图 3-4 所示。

3-4  ForumFactory 抽象工厂模式图

3-4 XmlForumFactory DbForumFactory 作为抽象工厂 ForumFactory 的两个具体实现 Forum ForumThread ForumMessage 分别作为 3 个系列抽象产品接口 依靠不同的工厂实现方式 会产生不同的产品对象。

从抽象工厂模式去理解 Jive 论坛统一入口处,可以一步到位掌握了几个类之间的大概关系。因为使用了抽象工厂模式这种通用的设计模式,可以方便源码阅读者快速地掌握整个系统的结构和来龙去脉,图 3-4 这张图已经初步展示了 Jive 的主要框架结构。

细心的读者也许会发现,在上面 ForumFactory 有一个 getInstance 比较令人费解,这将在 2.3 节进行讨论。


2.3  统一入口与单态模式

在上面 ForumFactory getInstance 方法使用单态( SingleTon )模式。单态模式是保证一个类有且仅有一个对象实例,并提供一个访问它的全局访问点。

前面曾提到 ForumFactory Jive 提供客户端访问数据库系统的统一入口。为了保证所有的客户端请求都要经过这个 ForumFactory ,如果不使用单态模式,客户端下列调用语句表示生成了 ForumFactory 实例:

ForumFactory factory = new DbForumFactory();

客户端每发生一次请求都调用这条语句,这就会发生每次都生成不同 factory 对象实例,这显然不符合设计要求,因此必须使用单态模式。

一般在 Java 实现单态模式有几种选择,最常用而且安全的用法如下:

public class Singleton {

   private Singleton(){}

   // 在自己内部定义自己一个实例,是不是很奇怪

   // 注意这是 private ,只供内部调用

   private static Singleton instance = new Singleton();

   // 这里提供了一个供外部访问本 class 的静态方法,可以直接访问

   public static Singleton getInstance() {

     return instance;

   }

}

单态模式一共使用了两条语句实现:第一条直接生成自己的对象,第二条提供一个方法供外部调用这个对象,同时最好将构造函数设置为 private ,以防止其他程序员直接使用 new Singleton 生成实例。

还有一种 Java 单态模式实现:

public class Singleton {

   private Singleton(){}

   private static Singleton instance = null;

   public static synchronized Singleton getInstance() {

     if (instance==null)

       instance new Singleton()

     return instance;

   }

在上面代码中,使用了判断语句。如果 instance 为空,再进行实例化,这成为 lazy initialization 。注意 getInstance() 方法的 synchronized ,这个 synchronized 很重要。如果没有 synchronized ,那么使用 getInstance() 在第一次被访问时有可能得到多个 Singleton 实例。

关于 lazy initialization Singleton 有很多涉及 double-checked locking (DCL) 的讨论,有兴趣者可以进一步研究。一般认为第一种形式要更加安全些;但是后者可以用在类初始化时需要参数输入的情况下。

Jive ForumFactory 中采取了后者 lazy initialization 形式,这是为了能够动态配置指定 ForumFactory 的具体子类。在 getInstance 中,从配置文件中获得当前工厂的具体实现,如果需要启动 XmlForumFactory ,就不必修改 ForumFactory 代码,直接在配置文件中指定 className 的名字为 XmlForumFactory 。这样通过下列动态装载机制生成 ForumFactory 具体对象:

Class c = Class.forName(className);

factory = (ForumFactory)c.newInstance();

这是利用 Java 的反射机制,可以通过动态指定 className 的数值而达到生成对象的方式。

使用单态模式的目标是为了控制对象的创建,单态模式经常使用在控制资源的访问上。例如数据库连接或 Socket 连接等。单态模式可以控制在某个时刻只有一个线程访问资源。由于 Java 中没有全局变量的概念,因此使用单态模式有时可以起到这种作用,当然需要注意是在一个 JVM 中。

2.4  访问控制与代理模式

仔细研究会发现,在 ForumFactory getInstance 方法中最后的返回值有些奇怪。按照单态模式的概念应该直接返回 factory 这个对象实例,但是却返回了 ForumFactoryProxy 的一个实例,这实际上改变了单态模式的初衷。这样客户端每次通过调用 ForumFactory getInstance 返回的就不是 ForumFactory 的惟一实例,而是新的对象。之所以这样做是为了访问权限的控制,姑且不论这样做的优劣,先看看什么是代理模式。

代理模式是属于设计模式结构型模式中一种,它是实际访问对象的代理对象,或者影子对象,主要达到控制实际对象的访问。这种控制的目的很多,例如提高性能等。即远程代理模式,这种模式将在以后章节讨论。

其中一个主要的控制目的是控制客户端对实际对象的访问权限。在 Jive 系统中,因为有角色权限的分别,对于 Forum ForumThread FroumMessage 的访问操作必须经过权限机制验证后才能进行。

ForumFactoryProxy 中的 createForum 方法为例,其实 ForumFactoryProxy 也是 FroumFactory 的一种工厂实现,它的 createForum 具体实现如下:

public Forum createForum(String name, String description)

            throws UnauthorizedException, ForumAlreadyExistsException

    {

        if (permissions.get(ForumPermissions.SYSTEM_ADMIN)) {

            Forum newForum = factory.createForum(name, description);

            return new ForumProxy(newForum, authorization, permissions);

        }

        else {

            throw new UnauthorizedException();

        }

}

在这个方法中进行了权限验证,判断是否属于系统管理员。如果是,将直接从 DbForumFactory 对象 factory 的方法 createForum 中获得一个新的 Forum 对象,然后再返回 Forum 的子类代理对象 ForumProxy 。因为在 Forum 中也还有很多属性和操作方法,这些也需要进行权限验证。 ForumProxy ForumFactoryProxy 起到类似的作用。

Jive 中有下列几个代理类:

·          ForumFactoryProxy :客户端和 DbForumFactory 之间的代理。客户端访问 DbForumFactory 的任何方法都要先经过 ForumFactoryProxy 相应方法代理一次。以下意思相同。

·          ForumProxy :客户端和 DbForum 之间的代理,研究 Forum 对象的每个方法,必须先看 ForumProxy 对象的方法。

·          ForumMessageProxy :客户端和 DbForumMessage 之间的代理。

·          ForumThreadProxy :客户端和 DbForumThread 之间的代理。

User Group 也有相应的代理类。

由以上分析看出,每个数据对象都有一个代理。如果系统中数据对象非常多,依据这种一对一的代理关系,会有很多代理类,将使系统变得不是非常干净,因此可以使用动态代理来代替这所有的代理类,具体实现将在以后章节讨论。

2.5  批量分页查询与迭代模式

迭代( Iterator )模式是提供一种顺序访问某个集合各个元素的方法,确保不暴露该集合的内部表现。迭代模式应用于对大量数据的访问, Java Collection API Iterator 就是迭代模式的一种实现。

在前面章节已经讨论过,用户查询大量数据,从数据库不应该直接返回 ResultSet ,应该是 Collection 。但是有一个问题,如果这个数据很大,需要分页面显示。如果一下子将所有页面要显示的数据都查询出来放在 Collection ,会影响性能。而使用迭代模式则不必将全部集合都展现出来,只有遍历到某个元素时才会查询数据库获得这个元素的数据。

以论坛中显示帖子主题为例,在一个页面中不可能显示所有主题,只有分页面显示,如图 3-5 所示。

3-5 中一共分 15 页来显示所有论坛帖子,可以从显示 Forum.jsp 中发现下列语句可以完成上述结果:

ResultFilter filter = new ResultFilter();  // 设置结果过滤器

   filter.setStartIndex(start);             // 设置开始点

   filter.setNumResults(range);          // 设置范围

   ForumThreadIterator threads = forum.threads(filter);  // 获得迭代器

   while(threads.hasNext){

       // 逐个显示 threads 中帖子主题,输出图 3-5 中的每一行

   }

3-5  分页显示所有帖子

上述代码中主要是从 Forum threads 方法获得迭代器 ForumThreadIterator 的实例 依据前面代理模式中分析、研究 Forum 对象的方法 首先是看 ForumProxy 中对应方法 然后再看 DbForum 中对应方法的具体实现。在 ForumProxy threads 方法如下

public ForumThreadIterator threads(ResultFilter resultFilter) {

     ForumThreadIterator iterator = forum.threads(resultFilter);

      return new ForumThreadIteratorProxy(iterator, authorization, permissions);

}

首先是调用了 DbForum 中具体的 threads 方法,再追踪到 DbForum 中看看,它的 threads 方法代码如下:

public ForumThreadIterator threads(ResultFilter resultFilter) {

// resultFilter 设置范围要求获得 SQL 查询语句

   String query = getThreadListSQL(resultFilter, false); 

   // 获得 resultFilter 设置范围内的所有 ThreadID 集合

   long [] threadBlock = getThreadBlock(query.toString(), resultFilter.getStartIndex());

   // 以下是计算查询区域的开始点和终点

   int startIndex = resultFilter.getStartIndex();

   int endIndex;

   // If number of results is set to inifinite, set endIndex to the total

  // number of threads in the forum.

   if (resultFilter.getNumResults() == ResultFilter.NULL_INT) {

     endIndex = (int)getThreadCount(resultFilter);

   }else {

     endIndex = resultFilter.getNumResults() + startIndex;

   }

   return new ForumThreadBlockIterator(threadBlock, query.toString(),

               startIndex, endIndex, this.id, factory);

}

ResultFilter 是一个查询结果类,可以对论坛主题 Thread 和帖子内容 Message 进行过滤或排序,这样就可以根据用户要求定制特殊的查询范围。如查询某个用户去年在这个论坛发表的所有帖子,那只要创建一个 ResultFilter 对象就可以代表这个查询要求。

在上面 threads 方法代码中,第一步是先定制出相应的动态 SQL 查询语句,然后使用这个查询语句查询数据库,获得查询范围内所有的 ForumThread ID 集合,然后在这个 ID 集合中获得当前页面的 ID 子集合,这是非常关键的一步。

在这关键的一步中,有两个重要的方法 getThreadListSQL getThreadBlock

·          GetThreadListSQL :获得 SQL 查询语句 query 的值,这个方法 Jive 实现起来显得非常地琐碎。

·          GetThreadBlock :获得当前页面的 ID 子集合,那么如何确定 ID 子集合的开始位置呢?查看 getThreadBlock 方法代码,可以发现,它是使用最普遍的 ResultSet next() 方法来逐个跳跃到开始位置。

上面代码的 Threads 方法中最后返回的是 ForumThreadBlockIterator ,它是抽象类 ForumThreadIterator 的子类,而 ForumThreadIterator 继承了 Collection Iterator ,以此声明自己是一个迭代器, ForumMessageBlockIterator 实现的具体方法如下:

public boolean hasNext();     // 判断是否有下一个元素

public boolean hasPrevious()  // 判断是否有前一个元素

public Object next() throws java.util.NoSuchElementException  // 获得下一个元素实例

ForumThreadBlockIterator 中的 Block 是“页”的意思,它的一个主要类变量 threadBlock 包含的是一个页面中所有 ForumThread ID next() 方法实际是对 threadBlock ForumThread 进行遍历,如果这个页面全部遍历完成,将再获取下一页( Block )数据。

ForumThreadBlockIterator 重要方法 getElement 中实现了两个功能:

·          如果当前遍历指针超过当前页面,将使用 getThreadBlock 获得下一个页面的 ID 子集合;

·          如果当前遍历指针在当前页面之内,根据 ID 获得完整的数据对象,实现输出;

ForumThreadBlockIterator getElement 方法代码如下:

private Object getElement(int index) {

   if (index < 0) {        return null;        }

   // 检查所要获得的 element 是否在本查询范围内(当前页面内)

   if (index < blockStart ||

index >= blockStart + DbForum.THREAD_BLOCK_SIZE) {  

      try {

          // 从缓冲中获得 Forum 实例

          DbForum forum = factory.cacheManager.forumCache.get(forumID);

          // 获得下一页的内容

          this.threadBlock = forum.getThreadBlock(query, index);

          this.blockID = index / DbForum.THREAD_BLOCK_SIZE;

          this.blockStart = blockID * DbForum.THREAD_BLOCK_SIZE;

      } catch (ForumNotFoundException fnfe) {

               return null;

       }

     }

     Object element = null;

     // 计算这个元素在当前查询范围内的相对位置

     int relativeIndex = index % DbForum.THREAD_BLOCK_SIZE;

     // Make sure index isn't too large

     if (relativeIndex < threadBlock.length) {

        try {

            // 从缓冲中获得实际 thread 对象

            element = factory.cacheManager.threadCache.get(

                        threadBlock[relativeIndex]);

        } catch (ForumThreadNotFoundException tnfe) { }

     }

     return element;

}

ForumThreadBlockIterator 是真正实现分页查询的核心功能, ForumThreadBlockIterator 对象返回到客户端的过程中,遭遇 ForumThreadIteratorProxy 的截获,可以回头看看 ForumProxy 中的 threads 方法,它最终返回给调用客户端 Forum.jsp 的是 ForumThreadIteratorProxy 实例。

ForumThreadIteratorProxy 也是迭代器 ForumThreadIterator 的一个子类,它的一个具体方法中:

public Object next() {

  return new ForumThreadProxy((ForumThread)iterator.next(), authorization,

            permissions);

}

这一句是返回一个 ForumThreadProxy 实例,返回就是一个 ForumThread 实例的代理。这里, Jive 使用代理模式实现访问控制实现得不是很巧妙,似乎有代理到处“飞”的感觉,这是可以对之进行改造的。

从以上可以看出, Jive 在输出如图 3-5 所示的多页查询结果时,采取了下列步骤:

1 )先查询出符合查询条件的所有对象元素的 ID 集合,注意不是所有对象元素,只是其 ID 的集合,这样节约了大量内存。

2 )每个页面视为一个 Block ,每当进入下一页时,获得下一个页面的所有对象的 ID 集合。

3 )输出当前页面的所有对象时,首先从缓冲中获取,如果缓冲中没有,再根据 ID 从数据库中获取完整的对象数据。

上述实现方法完全基于即查即显,相比于一般批量查询做法:一次性获得所有数据,然后遍历数据结果集 ResultSet Jive 这种批量查询方式是一种比较理想的选择。

以上是 ForumThread 的批量显示,有关帖子内容 ForumMessage 也是采取类似做法。在每个 ForumThread 中可能有很多帖子内容( ForumMessage 对象集合),也不能在一个页面中全部显示,所以也是使用迭代模式来实现的。显示一个 Forum 主题下所有帖子内容的功能由 ForumThread messages() 方法完成,检查它的代理类 FroumThreadProxy 如何具体完成:

public Iterator messages(ResultFilter resultFilter) {

   Iterator iterator = thread.messages(resultFilter);

   return new IteratorProxy(JiveGlobals.MESSAGE, iterator, authorization, permissions);

}

实现的原理基本相同,返回的都是一个 Iterator 代理类,在这些代理类中都是进行用户权限检验的。

Jive 中也有关于一次性获得所有数据,然后遍历 ResultSet 的做法。这种做法主要适合一次性查询数据库的所有数据,例如查询当前所有论坛 Forum ,首先实现 SQL 语句:

SELECT forumID FROM jiveForum

获得所有 Forum forumID ,这段代码位于 DbForumFactory.java forums 方法中,如 下:

  public Iterator forums() {

    if (forums == null) {

      LongList forumList = new LongList();

      Connection con = null;

      PreparedStatement pstmt = null;

      try {

        con = ConnectionManager.getConnection();

        // GET_FORUMS 值是 SELECT forumID FROM jiveForum

        pstmt = con.prepareStatement(GET_FORUMS);

        ResultSet rs = pstmt.executeQuery();

        while (rs.next()) {

          forumList.add(rs.getLong(1));                 // 将所有查询 ID 结果放入 forumList

        }

      }catch (SQLException sqle) {

        sqle.printStackTrace();

      } finally {

        …

    }

    return new DatabaseObjectIterator(JiveGlobals.FORUM, forums, this);

  }

forums 方法是返回一个 DatabaseObjectIterator ,这个 DatabaseObjectIterator 也是一个迭代器,但是实现原理要比 ForumThreadBlockIterator 简单。它只提供了一个遍历指针,在所有 ID 结果集中遍历,然后也是通过 ID 获得完整的数据对象。

总之, Jive 中关于批量查询有两种实现方式:以 ForumThreadBlockIterator 为代表的实现方式适合在数据量巨大、需要多页查询时使用;而 DatabaseObjectIterator 则是推荐在一个页面中显示少量数据时使用。

2.6  过滤器与装饰模式

装饰( Decorator )模式是动态给一个对象添加一些额外的职责,或者说改变这个对象的一些行为。这就类似于使用油漆为某个东西刷上油漆,在原来的对象表面增加了一层外衣。

在装饰模式中,有两个主要角色:一个是被刷油漆的对象( decoratee );另外一个是给 decoratee 刷油漆的对象( decorator )。这两个对象都继承同一个接口。

首先举一个简单例子来说明什么是装饰模式。

先创建一个接口:

public interface Work

{

   public void insert();

}

这是一种打桩工作的抽象接口,动作 insert 表示插入,那么插入什么?下面这个实现表示方形木桩的插入:

public class SquarePeg implements Work{

   public void insert(){

     System.out.println(" 方形桩插入 ");

   }

}

本来这样也许就可以满足打桩的工作需要,但是有可能土质很硬,在插入方形桩之前先要打一个洞,那么又将如何实现?可以编制一个 Decorator 类,同样继承 Work 接口,但是在实现 insert 方法时有些特别:

public class Decorator implements Work{

   private Work work;

   // 额外增加的功能被打包在这个 List

   private ArrayList others = new ArrayList();

   public Decorator(Work work)

   {

     this.work=work;

     others.add(" 打洞 ");   // 准备好额外的功能

   }

   public void insert(){

     otherMethod();

     work.insert();

   }

   public void otherMethod()

   {

     ListIterator listIterator = others.listIterator();

     while (listIterator.hasNext())

     {

       System.out.println(((String)(listIterator.next())) + " 正在进行 ");

     }

}

}

Decorator 的方法 insert 中先执行 otherMethod() 方法,然后才实现 SquarePeg insert 方法。油漆工 Decorator 给被油漆者 SquarePeg 添加了新的行为 —— 打洞。具体客户端调用如下:

Work squarePeg new SquarePeg();

Work decorator = new Decorator(squarePeg);

decorator.insert();

本例中只添加了一个新的行为(打洞),如果还有很多类似的行为,那么使用装饰模式的优点就体现出来了。因为可以通过另外一个角度(如组织新的油漆工实现子类)来对这些行为进行混合和匹配,这样就不必为每个行为创建一个类,从而减少了系统的复杂性。

使用装饰模式可以避免在被油漆对象 decoratee 中包装很多动态的,可能需要也可能不需要的功能,只要在系统真正运行时,通过油漆工 decorator 来检查那些需要加载的功能,实行动态加载。

Jive 论坛实现了信息过滤功能。例如可以将帖子内容中的 HTML 语句过滤掉;可以将帖子内容中 Java 代码以特别格式显示等。这些过滤功能有很多,在实际使用时不一定都需要,是由实际情况选择的。例如有的论坛就不需要将帖子内容的 HTML 语句过滤掉,选择哪些过滤功能是由论坛管理者具体动态决定的。而且新的过滤功能可能随时可以定制开发出来,如果试图强行建立一种接口包含所有过滤行为,那么到时有新过滤功能加入时,还需要改变接口代码,真是一种危险的行为。

装饰模式可以解决这种运行时需要动态增加功能的问题,且看看 Jive 是如何实现的。

前面讨论过,在 Jive 中,有主要几个对象 ForumFactory Forum 以及 ForumThread ForumMessage ,它们之间的关系如图 3-2 所示。因此帖子内容 ForumMessage 对象的获得是从其上级 FroumThread 的方法 getMessage 中获取,但是在实际代码中, ForumThread 的方法 getMessage 委托 ForumFactory 来获取 ForumMessage 对象。看看 ForumThread 的子类 DbForumThread getMessage 代码:

public ForumMessage getMessage(long messageID)

         throws ForumMessageNotFoundException

{

     return factory.getMessage(messageID, this.id, forumID);

}

这是一种奇怪的委托,大概是因为需要考虑到过滤器功能有意为之吧。那就看看 ForumFactory 的具体实现子类 DbForumFactory getMessage 功能, getMessage 是将数据库中的 ForumMessage 对象经由过滤器过滤一遍后输出(注:因为原来的 Jive getMessage 代码考虑到可缓存或不可缓存的过滤,比较复杂,实际过滤功能都是可以缓存的,因此精简如下)。

protected ForumMessage getMessage(long messageID, long threadID, long forumID)

            throws ForumMessageNotFoundException

{

        DbForumMessage message = cacheManager.messageCache.get(messageID);

        // Do a security check to make sure the message comes from the thread.

        if (message.threadID != threadID) {

            throw new ForumMessageNotFoundException();

        }

        ForumMessage filterMessage = null;

            try {

  // 应用全局过滤器

     filterMessage = filterManager.applyFilters(message);

                Forum forum = getForum(forumID);               

                // 应用本论坛过滤器

                filterMessage = forum.getFilterManager().applyFilters(filterMessage);

            }

            catch (Exception e) { }

        return filterMessage;

}

上面代码实际是装饰模式的客户端调用代码, DbForumMessage 的实例 message 是被油漆者 decoratee 。通过 filterManager forum.getFilterManager() applyFilter 方法,将 message 实行了所有的过滤功能。这就类似前面示例的下列语句:

Work decorator = new Decorator(squarePeg);

forum.getFilterManager() 是从数据库中获取当前配置的所有过滤器类。每个 Forum 都有一套自己的过滤器类,这是通过下列语句实现的:

FilterManager filterManager =  new DbFilterManager();

DbFilterManager 的类变量 ForumMessageFilter [] filters 中保存着所有的过滤器, applyFilters 方法实行过滤如下:

public ForumMessage applyFilters(ForumMessage message) {

    for (int i=0; i < filters.length; i++) {

         if (filters[i] != null) {

            message = filters[i].clone(message);

         }

     }

     return message;

}

ForumMessageFilter ForumMessage 的另外一个子类,被油漆者 DbForumMessage 通过油漆工 ForumMessageFilter 增加了一些新的行为和功能(过滤),如图 3-6 所示。

3-6  装饰模式

这就组成了一个稍微复杂一点的装饰模式。 HTMLFilter 实现了 HTML 代码过滤功能,而 JavaCodeHighLighter 实现了 Java 代码过滤功能, HTMLFilter 代码如下:

public class HTMLFilter extends ForumMessageFilter {

    public ForumMessageFilter clone(ForumMessage message){

        HTMLFilter filter = new HTMLFilter();

        filter.message = message;

        return filter;

    }

    public boolean isCacheable() {

        return true;

    }

    public String getSubject() {

        return StringUtils.escapeHTMLTags(message.getSubject());

    }

    public String getBody() {

        return StringUtils.escapeHTMLTags(message.getBody());

    }

}

HTMLFilter 中重载了 ForumMessage getSubject() getBody() 方法,实际是改变了这两个原来的行为,这类似前面举例的方法:

public void insert(){

     otherMethod();

     work.insert();

}

这两者都改变了被油漆者的行为。

HTMLFilter 中还使用了原型( Prototype )模式,原型模式定义是: 用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。按照这种定义, Java clone 技术应该是原型模式的一个实现。

HTMLFilter clone 方法实际就是在当前 HTMLFilter 实例中再生成一个同样的实例。这样在处理多个并发请求时,不用通过同一个过滤器实例进行处理,提高了性能。但是 HTMLFilter clone 方法是采取 new 方法来实现,不如直接使用 Object native 方法速度快。

因为在 DbFilterManager 中是根据配置使用类反射机制动态分别生成包括 HTMLFilter 在内的过滤器实例。但是每种过滤器实例只有一个,为了使得大量用户不必争夺一个过滤器实例来实现过滤,就采取了克隆方式,这种实战手法可以借鉴在自己的应用系统中。

2.7  主题监测与观察者模式

观察者( Observer )模式是定义对象之间一对多的依赖关系,当一个被观察的对象发生改变时,所有依赖于它的对象都会得到通知并采取相应行为。

使用观察者模式的优点是将被观察者和观察者解耦,从而可以不影响被观察者继续自己的行为动作。观察者模式适合应用于一些“事件触发”场合。

Jive 中, 用户也许会对某个主题感兴趣,希望关于此主题发生的任何新的讨论能通过电子邮件通知他,因此他订阅监视了这个主题。因为这个功能的实现会引入电子邮件的发 送。在前面章节已经讨论了电子邮件发送有可能因为网络原因延迟,如果在有人回复这个主题时,立即进行电子邮件发送,通知所有订阅该主题的用户。那么该用户 可能等待很长时间得不到正常回应。

使用观察者模式,可以通过触发一个观察者,由观察者通过另外线程来实施邮件发送,而被观察者发出触发通知后,可以继续自己原来的逻辑行为。

看看 Jive WatchManager 类:

public interface WatchManager {

    // 正常监察类型,用户在这个主题更新后再次访问时,会明显地发现

    public static final int NORMAL_WATCH = 0;

     // 当主题变化时,通过电子邮件通知用户

    public static final int EMAIL_NOTIFY_WATCH = 1;

    // 设置一个主题被观察的时间,默认为 30

    public void setDeleteDays(int deleteDays) throws UnauthorizedException;

    public int getDeleteDays();

    // 是否激活了 E-mail 提醒

    public boolean isEmailNotifyEnabled() throws UnauthorizedException;

    public void setEmailNotifyEnabled(boolean enabled) throws UnauthorizedException;

    // 保存 E-mail 的内容

    public String getEmailBody() throws UnauthorizedException;

    public void setEmailBody(String body) throws UnauthorizedException;

    // 保存 E-mail 的主题

    public String getEmailSubject() throws UnauthorizedException;

public void setEmailSubject(String subject) throws UnauthorizedException;

    …

 

    // 为某个主题创建一个观察者

    public void createWatch(User user, ForumThread thread, int watchType)

            throws UnauthorizedException;

    // 删除某个主题的观察者

    public void deleteWatch(User user, ForumThread thread, int watchType)

    // 得到一个主题的所有观察者

    public Iterator getWatchedForumThreads(User user, int watchType)

            throws UnauthorizedException;

    // 判断一个用户是否在观察监视该主题

    public boolean isWatchedThread(User user, ForumThread thread, int watchType)

            throws UnauthorizedException;

    …

}

DbWatchManager WatchManager 的一个子类,通过数据库保存着有关某个主题被哪些用户监视等数据资料。 WatchManager 对象是随同 DbForumFactory() 一起生成的。

DbWatchManager 中有一个 WatchManager 没有的很重要的方法 —— 通知方法:

protected void notifyWatches(ForumThread thread) {

     //If watches are turned on.

    if (!emailNotifyEnabled) {

            return;

     }

     // 通知所有观察这个主题的用户

     EmailWatchUpdateTask task = new EmailWatchUpdateTask(this, factory, thread);

     TaskEngine.addTask(task);

 }

这个方法用来触发所有有关这个主题的监视或订阅用户,以 E-mail 发送提醒他们。那么这个通知方法本身又是如何被触发的?从功能上分析,应该是在发表新帖子时触发。

DbForumThread addMessage 的最后一行有一句:

factory.watchManager.notifyWatches(this);

这其实是调用了 DbWatchManager notifyWatches 方法,因此确实是在增加新帖子时触发了该帖子的所有观察者。

notifyWatches 方法中在执行 E-mail 通知用户时,使用了 TaskEngine 来执行 E-mail 发送。 E-mailWatchUpdateTask 是一个线程类,而 TaskEngine 是线程任务管理器,专门按要求启动如 E-mailWatchUpdateTask 这样的任务线程。其实 TaskEngine 是一个简单的线程池,它不断通过查询 Queue 是否有可运行的线程,如果有就直接运行线程。

public class TaskEngine {

    // 任务列表

    private static LinkedList taskList = null;

    // 工作数组

    private static Thread[] workers = null;

    private static Timer taskTimer = null;

    private static Object lock = new Object();

 

    static {

        // 根据配置文件初始化任务启动时间

        taskTimer = new Timer(true);

        // 默认使用 7 个线程来装载启动任务

        workers = new Thread[7];

        taskList = new LinkedList();

        for (int i=0; i<workers.length; i++) {

             // TaskEngineWorker 是个简单的线程类

            TaskEngineWorker worker = new TaskEngineWorker();

            workers[i] = new Thread(worker);

            workers[i].setDaemon(true);

            workers[i].start();         // 启动 TaskEngineWorker 这个线程

        }

    }

    //TaskEngineWorker 内部类

    private static class TaskEngineWorker implements Runnable {

        private boolean done = false;

        public void run() {

            while (!done) {

                // 运行 nextTask 方法

                nextTask().run();

            }

        }

    }

    // nextTask() 返回的是一个可运行线程,是任务列表 Queue 的一个读取者

    private static Runnable nextTask() {

        synchronized(lock) {

            // 如果没有任务,就锁定在这里

            while (taskList.isEmpty()) {

                try {

                    lock.wait();        // 等待解锁

                } catch (InterruptedException ie) { }

            }

            // 从任务列表中取出第一个任务线程

            return (Runnable)taskList.removeLast();

        }

    }

    public static void addTask(Runnable r) {

        addTask(r, Thread.NORM_PRIORITY);

    }

    // 这是任务列表 Queue 的生产者

    public static void addTask(Runnable task, int priority) {

        synchronized(lock) {

            taskList.addFirst(task);

            // 提醒所有锁在 lock 这里的线程可以运行了

            // 这是线程的互相通知机制,可参考线程参考资料

            lock.notifyAll();

        }

    }

    …

}

TaskEngine 中启动设置了一个消息管道 Queue 和两个线程。一个线程是负责向 Queue 里放入 Object ,可谓是消息的生产者;而另外一个线程负责从 Queue 中取出 Object ,如果 Queue 中没有 Object ,那它就锁定( Block )在那里,直到 Queue 中有 Object ,因为这些 Object 本身也是线程,因此它取出后就直接运行它们。

这个 TaskEngine 建立的模型非常类似 JMS Java 消息系统),虽然它们功能类似,但不同的是: JMS 是一个分布式消息发布机制,可以在多台服务器上运行,处理能力要强大得多。而 TaskEngine 由于基于线程基础,因此不能跨 JVM 实现。可以说 TaskEngine 是一个微观组件,而 JMS 则是一个宏观架构系统。 JMS 相关讨论将在后面章节进行。

以上讨论了 Jive 系统中观察者模式的实现, Jive 使用线程比较基础的概念实现了观察者模式,当然有助于了解 J2EE 很多底层的基础知识,整个 Web 容器的技术实现就是基于线程池原理建立的。

Java JDK 则提供了比较方便的观察者模式 API——java.util.Observable java.util.Observer ,它们的用户非常简单,只要被观察者继承 Observable ,然后使用下列语句设置观察点:

setChanged();

notifyObservers(name); // 一旦执行本代码,就触发观察者了

而观察者只要实现 Observer 接口,并实现 update 方法,在 update 方法中将被观察者触发后传来的 object 进行处理。举例如下:

网上商店中商品价格可能发生变化,如果需要在价格变化时,首页能够自动显示这些降价产品,那么使用观察者模式将方便得多。首先,商品是一个被观察者:

public class product extends Observable{

   private float price;

   public float getPrice(){ return price;}

   public void setPrice(){

   this.price=price;

  // 商品价格发生变化,触发观察者

   setChanged();

   notifyObservers(new Float(price));

   }

   ...

}

价格观察者实现 observer 接口:

public class PriceObserver implements Observer{

   private float price=0;

   public void update(Observable obj,Object arg){

     if (arg instanceof Float){

     price=((Float)arg).floatValue();

     System.out.println("PriceObserver :price changet to "+price);

     }

   }

}

这样,一个简单的观察者模式就很容易地实现了。

posted on 2006-08-31 12:26 Alex 阅读(743) 评论(0)  编辑  收藏 所属分类: java

只有注册用户登录后才能发表评论。


网站导航: