Matcher-Handler(匹配器-处理器)模式

Frank Metayer著  

 

意图

Matcher-Handler模式定义了一个普遍的机制,用一种松耦合的方式将原始数据分发传递给一个或多个数据处理器。这个模式显式地将数据鉴别匹配(Match)的责任从数据处理(Handle)中分离出来。

动机

许多工业用的电脑在输入设备的配备上都有多样性,它们可以从周围环境中接受多种不同类型的输入数据。比如说,象现金出纳机和抽奖终端等“售货点”型设备通常都配备有一个或多个光学扫描设备。现金出纳机上的光学扫描设备是一个条码阅读器,它将读出商店的货物、优惠券、消费卡……上的条码数据。当它扫描到一个条码之后,出纳机的前端应用程序必须对原始数据进行足够的处理以确定数据的类别(例如:优惠券),然后将数据传递给合适的应用对象进行处理。开发一个有普遍性的数据路由框架来分发这些原始数据对于其他应用的开发是很有好处的。

       这样的一个框架应该有如下的特征:对所有输入设备——不管它们输入的数据是何种类型——提供一致的行为;明确定义应用编程接口(API)以引入对新的数据格式的支持;在增加对新的数据格式的支持时,将同时引入bug的风险减至最小。

       Matcher-Handler模式可以用做开发这样一个框架的基础,因为它概括了数据识别和数据分发的方法。上面描述的条码例子中相关的类可见图1(见下页)。


1条码数据处理器类关系图

       BarcodeReader实现了应用程序的前端部分,其职责是从输入设备接收数据[1]并把数据分发给适当的实体进行处理。它的实现是普遍的、可扩展的。它管理了一个“匹配器/处理器”二元组列表,并将所有的数据识别及处理都分别推迟分配给这些对象。

       BarcodeDataMatcherBarcodeDataHandler这两个抽象类定义了对数据进行匹配和处理所需的普遍接口。具体的匹配器——比如CustomerCardMatcher——对原始数据进行查询以判别数据是否与特定的规则相匹配(这些规则包括数据的尺寸、数据头的内容等等)。如果数据通过了验证,则函数isMatch()返回true;否则返回false。而具体的处理器——象CustomerCardHandler——只有当数据被确定为它们特定的类型时才会被通知。

       BarcodeReader::deliverData()方法的伪码中我们可以看到,可能有多个处理器同时接收到同样的数据。这对某些数据处理的方法可能是有利的。但是你也可能你不希望使用这种分发数据的方式,那么你可以重新构建这个for循环以保证只有一个处理器接收到数据。(BarcodeReader一种更为灵活的实现方式是:把for循环的行为提取到BarcodeReader类的外部,放置到一个strategy [Gamma+95] 对象中,并在BarcodeReader被创建时将strategy的引用传递给它。)

适用性

Matcher-Handler模式适用于以下的情况:

l         在不明确指定接收者的情况下分发数据。

l         需要有多个对象同时而又各自独立的处理同一个数据事件。

l         需要动态指定处理离散数据事件的对象。

l         需要用一个处理器处理来自多个数据源的数据。

l         你希望在未来可以很方便地为这个应用程序引入新的数据类型。

结构

参见图2


:类关系图

参与者

l         ConcreteDataSource (BarcodeReader) ——具体数据源
-
对数据源(例如:输入设备)进行控制,接收或产生数据事件。当它要开始给数据处理器传递数据时,调用自己的deliverData()方法。
-
查询匹配器以确定与之相关的处理器是否应该接收一个数据,然后把数据传递给合适的(一个或多个)处理器。
-
通过“注册方法”addHandler()removeHandler()管理一个匹配器/处理器二元组序列。

l         Matcher ( BarcodeDataMatcher) ——匹配器
-
定义一个接口,数据源用这个接口来鉴别一个特定的数据事件是否与一个特定的处理器需要的标准相匹配。

l         ConcreteMatcher (CustomerCardMatcher) ——具体匹配器
-
实现匹配器接口以鉴别数据是否符合标准。

l         Handler ( BarcodeDataHandler) ——处理器
-
声明一个接口,数据源用这个接口把数据传递给处理器。

l         ConcreteHandler (CustomerCardHandler) ——具体处理器
-
处理数据源按照应用程序定义的某种途径传递来的数据。

协作

1.        ConcreteDataSource向每个匹配器询问一个数据事件是否与它们各自包装的匹配标准相匹配。

2.        如果数据与某个匹配器的标准匹配(即:isMatch()返回true),ConcreteDataSource将数据传递给与此匹配器相连的处理器。此处理器将处理这些数据。

3.        如果数据与某个匹配器的标准不匹配,与之相连的处理器不会得到通知。ConcreteDataSource将查询列表中的下一个匹配器。

效果

Matcher-Handler模式有以下优点:

l         降低耦合度。使用这个模式,数据源不必知道“哪个对象可以处理哪种数据事件”这样的事情。现在数据源只知道数据被应用程序接收或忽略。数据源和数据处理器不再需要对对方有任何显式的了解。

l         数据处理的运行时适应性。因为处理器序列是动态指定的,所以应用程序可以在任何时候更换当前使用的处理器。如果一些处理需求发生变化,应用程序可以增加或移除处理器。借助Memento模式[Gamma+95]或者其他相似的机制,应用程序还可以再恢复以前的处理器配置。

l         使应用程序容易被扩展。Matcher-Handler模式提供了一个响应自发数据事件的框架的基础,对于这个框架来说,温和地忽略未被承认的或尚不支持的数据也是很重要的。这在一个全新的应用程序的开发早期是特别重要的,在那个时候,整个应用程序的全貌还没有呈现出来。当你创建一个新的对象以支持一种新的输入数据类型时,这个对象已经被注册到合适的数据源、可以开始接收输入数据了。

l         减少了引入新功能带来的风险。为支持新的输入数据类型而引入的代码不会要求数据源的代码做一行改动。这也就是说,Matcher-Handler模式可以用于开发具有扩展性的应用框架,而且此框架可以以二进制形式(而不是源代码形式)发布。更重要的是,“引入新功能而不需改变数据源”意味着“引入新的处理器时破坏原先存在的代码”这样的风险被大大降低了。

Matcher-Handler有这样一个缺点:

       匹配器可能对其它的匹配器有过多的了解。如果数据不是作为其原来的对象、而是作为简单数据被处理,则具体的匹配器会知道其他匹配器针对特定数据源的实现。导致这种情况的原因是,一个匹配器必须知道与它匹配的数据相对于其他数据的特点。所以,它一定会知道其他数据类型的一些情况。

       举例来说,仓储产品的条码数据是标识这种产品的一个数字,用户信用卡的条码数据可能包含一个消费者的名字、住址等等信息。如果预先规定产品标志总是被编码为8字节的整数、用户信息总是被编码为40字节的整数,那么这个应用程序将只能处理这两种数据,匹配器也只能根据数据的长度来实现其匹配细节。

       当不得不引入新的数据类型时,这种假设将被打破。由于以前的假设被打破,匹配器将必须用新的匹配标准重新实现。但是请注意,这个问题不会影响数据处理过程,因为处理过程被隔离到另一个类——处理器——中了。这正是我们最初、最主要的动机:将匹配器的实现从处理器的实现中分离出来。

实现

在实现Matcher-Handler模式时,应该牢记以下几个要点:

l         在“动机”一节中介绍的例子声明了匹配器的接口,这个接口返回一个boolean值,以此指出一个数据是否应该被传递给与匹配器相连的处理器。但在一个正规的应用程序中,匹配器最好能返回一个更高级的对象。当数据通过验证时,这个对象将原始数据包装在其中;当数据未通过验证时,匹配器返回null(或者一个Null对象[PIoPD3])。数据源将这个匹配器返回的对象传递给处理器。这样,处理器不需要重新解释(甚至不需要知道)原始数据。同时,如果处理器操作高级对象而不是原始数据,它可以对这些对象进行多态操作,而这些对象的具体类型只有匹配器才知道。


3:匹配器的变化

l         在同一个类中实现匹配器和处理器的接口,以使一个对象可以进行匹配和处理的操作。这将减少应用程序中类与对象的数目及——外观上的——复杂性,但会带来很大的风险。如果一个对象同时可以进行匹配和处理的操作,在实现这些行为时就会有这样的倾向:匹配器通过实例变量向处理器传递信息。当新的数据类型被引入、匹配标准必须改变(参见“效果”一节)的时候,由于两个方法之间微妙的依赖的影响,很有可能你必须同时修改数据处理的代码。

l         当数据的处理过程可能会耗费相当长的时间时,deliverData()方法更好的实现可能是:一次性查询所有的匹配器,然后创建一个应该接收数据的处理器的列表;将数据传递给这些处理器之后,将它们的处理过程放到一个优先级较低的后台线程中。用这样的实现方法,对deliverData()方法的调用将更快得到回应。这将使数据源可以马上向用户提供一个声音和/或图象的反馈,以提示用户数据是否被接收或拒绝。(deliverData()的行为方式是由程序员决定的,它受很多因素影响。可以考虑用Strategy[Gamma+95]模式实现deliverData()方法。)

l         如果一个应用程序中有几个不同但是相似的数据源,为ConcreteDataSource引入一个抽象的超类是有用的。这个新的超类可以实现匹配器/处理器的注册和数据分发等行为。具体的子类则把注意力集中在输入设备的控制上。抽象超类的加入使数据处理器接收来自不同数据源的数据变得简单。举例来说,一个单独的处理器实体可以接收并处理所有的数据,不论它们来自条码阅读器、磁性条纹阅读器、还是智能卡阅读器,而且处理器不必为任何输入设备做任何的准备。

 


4:抽象数据源

l         数据源用Pair对象来维护匹配器和处理器之间的联系,这只是连接这两个对象的几种方法中的一种。这个变化提供了相当大的适应性,因为它允许一个处理器的实例被注册给几个不同的匹配器、或是一个匹配器的实例被注册给几个不同的处理器。如果这个多对多的关联不是你需要的,那么请考虑使用这种方法:把一个对象的显式引用提供给另一个,从而把匹配器和处理器连接起来。下面的图显示了这样的情况:每个匹配器拥有与之相连的处理器的显式引用。


5:匹配器拥有处理器的引用

代码示例

这是“动机”一节中讨论的“条码阅读器”(参见图1)的一个java实现。示例代码从BarcodeReader开始。因为我们不想在这里讨论具体的输入设备,所有控制输入设备的代码都被省去了。

public class BarcodeReader extends Thread

{

private Vector _handlers;

// Matcher/Handler registration.

public void addHandler( BarcodeDataMatcher matcher,

BarcodeDataHandler handler )

{

_handlers.addElement( new Pair(matcher,handler) );

}

// Deliver data to the appropriate handler.

protected boolean deliverData( byte[] data )

{

Enumeration enum = _handlers.elements();

boolean delivered = false;

while( enum.hasMoreElements() )

{

Pair pair = (Pair) enum.nextElement();

if( pair.getMatcher().isMatch(data) )

{

pair.getHandler().process( data );

delivered = true;

}

}

return delivered;

}

// Device Thread. Accept data and deliver it, forever.

public void run()

{

while( true )

{

byte[] data = _waitForInput();

boolean delivered = deliverData( data );

if( delivered )

_flashGreen();

else

_flashRed();

}

}

...

}

       下面的两个类是匹配器接口和一个具体的匹配器实现。这个匹配器以数据长度为根据,判断数据是否是一个产品标志号。

public interface BarcodeDataMatcher

{

public boolean isMatch( byte[] data );

}

public class ProductNumberMatcher implements BarcodeDataMatcher

{

public boolean isMatch( byte[] data )

{

return data.length == 8;

}

}

       下面的两个类是处理器接口和一个具体处理器的例子。这个产品标志处理器根据标志号造出新的产品实例(用factory method[Gamma+95]模式),然后根据一个销售者名单(singleton[Gamma+95]模式)邮寄产品实例。

public interface BarcodeDataHandler

{

public void process( byte[] data );

}

public class ProductNumberHandler implements BarcodeDataHandler

{

public void process( byte[] data )

{

// 'data' is an ASCII encoded integer

String ascii = new String( data );

Integer pid = Integer.decode( ascii );

Product prod = Product.newInstance( pid );

Sales.getInstance().add( prod );

}

}

       主程序代码可能会是这样:

public class Application

{

public static void main( String[] args )

{

BarcodeReader reader = new BarcodeReader();

BarcodeDataMatcher matcher;

BarcodeDataHandler handler;

matcher = new ProductNumberMatcher();

handler = new ProductNumberHandler();

reader.addHandler( matcher, handler );

matcher = new CustomerCardMatcher();

handler = new CustomerCardHandler();

reader.addHandler( matcher, handler );

// add more handlers...

reader.start();

}

}

已知应用

l         Altura[2]图象阅读器。这是一个在诸如收据、游戏名单、游戏者注册卡等文本上扫描的图象阅读器。扫描器获得的数据通过一系列的匹配器-处理器二元组被传递给应用程序。一个类型的数据只被分发给要求得到它们的处理器。处理器在一个独立的后台进程中被调用,所以图象阅读器可以很快的向用户提供接收或拒绝的反馈。

l         Altura通信框架。在抽奖终端运行之中,它将从中心计算机系统那里收到很多它不请自到的消息。这些消息包括新闻、游戏结束通知、获胜号码公告等等。处理这些类型的消息的处理器每一个都和其他的处理器有很大的区别。举例来说,游戏结束通知会被分发给一个运行在终端上的游戏原型(prototype[Gamma+95]),这个原型将告诉游戏者“游戏已经结束了”;获胜号码公告可能会被分发给另一个游戏原型,这个原型将发出“现在将重新开始一局新的游戏”的信号。在这个实现中,只有一个匹配器进行消息的匹配。这个匹配器读入消息报文的报头信息,并根据报头的内容进行分发。

l         Windows注册表。当一个windows95/NT应用程序被安装到计算机上的时候,它将在注册表中唯一标志自己。应用程序告知注册表:对于在某个特定的文件类型上的OpenPrint等等请求,它可以处理。当用户提出对一个指定的文件的Open请求时,windows判别文件的类型(根据文件扩展名)、并与注册表内记录的文件类型做匹配。如果找到一个匹配的结果,相应的应用程序——它已经注册支持Open操作——将被调用以处理这个Open请求。

l         Smalltalk的方法选择。当一个消息被传送给一个对象时,Smalltalk会用一种名叫“方法查找”的机制来决定调用哪个方法来处理这个消息。Smalltalk对消息的标记和对象的类中定义的方法标记进行比较。如果找到一个匹配的方法标记,Smalltalk将调用与此标记对应的方法来处理这个消息。

l         远程过程调用(RPC)服务分派。当一个服务器收到一个远程过程程调用请求时,一个分派器会识别出本地机器上可以为这个请求服务的过程,并调用这个过程来处理这个请求。

相关模式

       Observer模式[Gamma+95]相似,Matcher-Handler的主要工作是分发信息,使这些信息看起来就象是发生在它们感兴趣的对象身上,尽管这些对象实际分布于整个应用程序之中。

       Observer的焦点在于:当某个目标的状态发生改变时通知应用程序对象。目标的状态发生任何改变,其观察者都将被通知到,而不是仅仅在观察者关心的改变发生时才通知。与之形成对比的是,Matcher-Handler模式中的处理器(观察者)只有在它们等待的特定信息到达数据源(目标)时才会被通知到。

       Matcher-Handler模式中的匹配器对象是Strategy[Gamma+95]模式的一个范例,尽管它们的动机有一些差别。Strategy模式的目标是——特别是在运行时——提供可选择的行为。匹配器对象的动机则是来自如下的事实:匹配标准在应用程序的整个生命周期中都有可能变化,将匹配行为隔离出来对于减少匹配行为的改变对系统其他部分的影响是有意义的。

       Matcher-Handler模式和Chain of Responsibility[Gamma+95]模式有相似之处:避免数据源(client)与其处理者的耦合,允许多个对象处理一个请求,准许动态指定处理者。Matcher-Handler模式的结构与Chain of Responsibility也有不同之处:Matcher-Handler中处理器的先后顺序取决于数据源;而Chain of Responsibility中处理器的顺序是预先确定的,并且由相连的处理器共同维护。

参考文献

[Gamma+95] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.中文版:《设计模式:可复用面向对象软件的基础》,李英军等译,机械工业出版社20009月。

[PloPD3] R. Martin, D. Riehle, F. Buschmann. Pattern Languages of Program Design. Reading, MA: Addison-Wesley, 1998.



[1]通常此数据不是以它自己的对象形式出现,而是作为简单的原始数据出现并被鉴别的。

[2] Altura™是由GTECH公司开发的一个抽奖终端。驱动这个终端的应用程序是一个基于框架的面向对象的JAVA软件。