(原文出处:http://today.java.net/pub/a/today/ 2004/10/29 /patterns.html)
OOP语言提供给我们最有用的东西可能就是多态了。比如说:在不知道一个对象具体类型的情况下向这个对象发送消息的能力。事实上,没有比Strategy模式更能说明这一点的模式了。
为了说明Strategy模式,让我们假设我们正在设计一个debug logger。通常Debug logger是一个非常有用的工具,程序员可以在代码中需要的地方向logger发送消息。如果系统在某些方面行为不正常,debug log可以给我们一些提示,告诉我们在系统失败的时候系统内部发生了什么。
为了保持这个功能的高效,logger的使用必须尽量简单。程序员们并不喜欢使用那些不方便的东西。在你发送消息时,你肯定更加喜欢如下简单的方式:
logger.log("My Message");
另一方面,我们希望从log里面看到的东西应该是有用的,一般来说那样的东西会比较复杂,至少我们希望看到消息产生的时间和日期,同时我们希望看到产生消息的线程ID。甚至,可能存在一个发送消息到log的系统的系统状态的详细清单。所以,logger需要搜集所有这些外部信息,并且按照log信息的格式将其格式化,然后将结果添加到不断增长的log信息的列表中去。
这些log信息应该放到哪里呢?有时我们可能希望放到一个文本文件中,有时我们可能希望放到一个数据库里,而有时我们可能仅仅希望他们被存储在RAM中。这个需求看起来永远是那么的飘忽不定,但是有一点始终是确定的,那就是消息最终的存放地和已经格式化了的log消息本身并没有任何关系。
我们有两个算法:一个是格式化log信息的算法(format algorithm),另一个是记录log消息的算法(record algorithm)。这两个算法其实都是存在于log消息中的数据流,但是它们两个都是独立的,可以任意替换(也就是说,format算法可以有很多种实现,而record算法也可以有很多种实现方法)。Format算法并不关心信息的具体存储方式,record算法也不关心format算法的细节。任何时候,我们有两个既有关联又相互独立的算法时,我们可以使用Strategy模式来连接他们。考虑以下结构:
现在用户调用了Logger类的log()函数。Log()函数首先格式化log信息,然后调用Recorder类的record()函数。这里可以有很多种方法来实现Recorder类,它们以不同的方式来完成记录工作。
Logger类和Recorder可以通过以下单元测试来作为例子,该单元测试使用了Adapter模式。
public class LoggerTest extends TestCase {
private String recordedMessage;
protected String message;
public void testLogger() throws Exception {
Logger logger = new Logger(new Recorder() {
public void record(String message) {
recordedMessage = message;
}
});
message = "myMessage";
logger.log(message);
checkFormat();
}
private void checkFormat() {
String datePattern = "//d{2}///d{2}///d{4} //d{2}://d{2}://d{2}.//d{3}";
String messagePattern = datePattern + " " + message;
if(!Pattern.matches(messagePattern, recordedMessage)) {
fail(recordedMessage + " does not match pattern");
}
}
}
正如你所看到的,这里创建了一个Logger类对象,它带有一个Recorder对象。Logger类并不关心具体该Recorder对象的具体实现是什么,它只是简单的构造了一个将被log的字符串,然后调用record()函数。这里非常的松耦合,它允许format算法和record算法在各自的领域里面和其他具有相同功能的对象相互替换。
Logger是一个比较简单的类,它只是格式化消息,然后把消息发送给Recoder。
public class Logger {
private Recorder recorder;
public Logger(Recorder recorder) {
this.recorder = recorder;
}
public void log(String message) {
DateFormat format = new SimpleDateFormat("MM/dd/yyyy kk:mm:ss.SSS");
Date now = new Date();
String prefix = format.format(now);
recorder.record(prefix + " " + message);
}
}
同时Recoder也是一个很简单的接口。
public interface Recorder {
void record(String message);
}
Strategy模式规范的形式如下。Strategy通过接口把Context中算法的具体实现给屏蔽了。Context并不关心Strategy怎么实现的算法,或者Strategy具体有多少种具体的实现。通常,Context会保持着一个指向Strategy对象的一个指针。
在我们的Logger例子中,类Logger是一个Context,类Recorder是Strategy接口,单元测试中的那些匿名的类扮演了一个具体的strategy。
如果你对OOP编程不陌生的话,那么你应该多次看见这个模式了。事实上,它是那么的普通以至于一些人们总是摇着头想知道为什么它还有个名字。它甚至有点像是给程序处理完一个模块后又去处理另一个模块的行为取了一个“做下一个模块”的名字。然而,这里仍然有一个很好的理由要给这个模式取个名字,我们还有另外一个模式解决了差不多同样问题的,为了区分它们俩,我们就给这个模式取了这个名字。
这另外一个模式就叫做Template Method。过在给Logger例子添加另外一层多态以后我们就可以看到Template Method了。目前我们已经有了一层多态,这层多态允许我们更改log信息被记录的方式。我们还可以添加另外一个层多态,那样的话程序就允许我们动态更改我们format信息的方式。让我们假设一下,对对象来说,我们希望支持两个格式化算法,一个预先设计了时间和日期而另外一个之设计时间却没有日期。
很清晰,这仅仅是另外一个多态的问题,我们可以再次使用Strategy模式。如果我们那样做,那么设计框架将如表现为如下形式:
这里我们两次使用了Strategy模式。一个提供记录多态和一个提供格式化多态。这个已经是一个很好的设计了,但是他并不是唯一的设计。事实上,我们有一个可选的设计,如下:
注意Logger类的format方法。它是一个protected(用#号表示)类型的并且是一个虚函数(用斜体表示)。Logger类的log()函数调用Logger类的虚函数format(),而该format()函数的具体实现则由Logger类的某一个子类来决定。然后格式化好了的string将被传递给Recorder类的record()函数。
考虑以下如下测试单元。它给我们展示了类TimeLogger和类TimeDataLogger。注意这两个测试函数各自都产生了一个适合自己的Logger子类对象,然后传递给Recorder对象。
import junit.framework.TestCase;
import java.util.regex.Pattern;
public class LoggerTest extends TestCase {
private String recordedMessage;
protected String message;
private static final String timeDateFormat =
"//d{2}///d{2}///d{4} //d{2}://d{2}://d{2}.//d{3}";
private static final String timeFormat = "//d{2}://d{2}://d{2}.//d{3}";
private Recorder recorder = new Recorder() {
public void record(String message) {
recordedMessage = message;
}
};
public void testTimeDateLogger() throws Exception {
Logger logger = new TimeDateLogger(recorder);
message = "myMessage";
logger.log(message);
checkFormat(timeDateFormat);
}
public void testTimeLogger() throws Exception {
Logger logger = new TimeLogger(recorder);
message = "myMessage";
logger.log(message);
checkFormat(timeFormat);
}
private void checkFormat(String prefix) {
String messagePattern = prefix + " " + message;
if (!Pattern.matches(messagePattern, recordedMessage)) {
fail(recordedMessage + " does not match pattern");
}
}
}
然后我们改写Logger类如下,注意protected类型的虚函数format()。
public abstract class Logger {
private Recorder recorder;
public Logger(Recorder recorder) {
this.recorder = recorder;
}
public void log(String message) {
recorder.record(format(message));
}
protected abstract String format(String message);
}
类TimeLogger和类TimeDataLogger仅仅是实现了适合他们自己的format()函数,如下:
import java.text.*;
import java.util.Date;
public class TimeLogger extends Logger {
public TimeLogger(Recorder recorder) {
super(recorder);
}
protected String format(String message) {
DateFormat format = new SimpleDateFormat("kk:mm:ss.SSS");
Date now = new Date();
String prefix = format.format(now);
return prefix + " " + message;
}
}
import java.text.*;
import java.util.Date;
public class TimeDateLogger extends Logger {
public TimeDateLogger(Recorder recorder) {
super(recorder);
}
protected String format(String message) {
DateFormat format = new SimpleDateFormat("MM/dd/yyyy kk:mm:ss.SSS");
Date now = new Date();
String prefix = format.format(now);
return prefix + " " + message;
}
}
Template Method模式规范的结构如下:
类Context至少包含两个函数,一个(这里称作function)就是普通的public函数,它代表上层(high-level)算法。另外一个(这里称作SubFunction)就是虚函数,它代表了底层算法,并且他被上层算法调用。类Context的派生类将会继承subFunction(),并且已不同的方式来实现它。
你必须清楚Strategy模式和Template Method模式是怎么解决相同的问题的。通过简单的把问题划分为上层算法和底层算法,我们可以发现然后这两个模式可以独立的更改变化。在这种情况下,Strategy模式为底层的算法创建一个接口(interface),而Template Method模式则创建一个虚函数。
与Template Method比起来,Strategy模式的优势就是:当底层算法需要在运行时期更改的时候,运用Strategy模式可以很好的解决。通过包含一个指向不同Strategy子类对象的Strategy指针,我们可以很好的完成这一任务。Template Method模式就没有这么幸运了,一旦对象被创建以后,Template Method模式中的底层算法就锁定了。另一方面,比起Template Method模式来,Strategy模式在时间和空间方面都比Template Method要差一些,并且Strategy模式比Template Method模式要复杂。所以,当需求很复杂的时候使用Strategy模式,当对时间和空间要求很苛刻的时候才使用Template Method模式。
我们能够通过使用两次Template Method模式来解决Logger的问题吗?可以,但是并不是很好。考虑以下框架:
注意这里每一种可能的组合情况(即format()和recort()函数的不同实现之间的组合)都必须有一个派生类。这就是令人感到恐惧的m*n的问题。对于任意两个给定的多态层次,派生类的数量就是这两个层次各自派生类数目的乘积。
这个问题很普通,普通到我们可以通过联合Strategy模式和Template Method模式来解决(就象我们在前面所作的那样),而组合以后的模式有一个很好听的名字——Bridge模式。