软件设计中最重要的一条原则就是“开-闭”原则(Open for extension, Closed for modification)。符合“开-闭”原则的系统,通常在稳定性、可扩展性、可插拔性等方面有非常良好的表现。实现“开-闭”原则,抽象化是关键,良好封装“可变”、“易变”部分是主要手段。我们要深刻体会面向抽象编程的本质和方法。
“高内聚、低耦合”是另一个大的设计原则。
最近从若干源码中看到几个小例子,摘录在这儿。
1. Log4j中的FileWatchdog
Log4j的源码,看到FileWatchdog相关的一部分代码。这个工具类可以自己启动一个线程,循环检查某一文件内容是否被修改,如果被修改,则触发相应的业务逻辑。
从功能上看,启动线程,定期循环检查文件内容是否发生变化这两部分基本是固定的、不变的。但是发现修改之后要触发的业务动作是可变、易变的。所以把后者封装成一个抽象的方法,在子类中延迟实现。与此同时,父类中的主流程面向抽象方法进行搭建。
该类是一个抽象类,需要根据具体的业务逻辑扩展自己的子类,实现具体的doOnChange()方法。
1.1 Class Diagram
1.2 FileWatchdog的相关方法
1.2.1 run()
While循环,每Sleep一段时间,检查被观察文件是否被修改。
while(!interrupted){
sleep(delay);
checkAndConfigure();
}
1.2.2 checkAndConfigure()
检查文件是否被修改,如果被修改,则调用doOnChange()方法。
fileExists = file.exists();
if (fileExists) {
if(file.lastModified()>lastModif){
doOnChange();
}
else{
log.warn(file doesn’t exit);
}
}
1.2.3 doOnChange()
这是一个抽象方法,具体的操作应有具体子类实现。
1.3 FileWatchdog的使用
首先如第1.1.1节所示,扩展一个具体的子类,实现doOnChange()方法。比如Log4j中,有一个XMLWatchdog类:
public class XMLWatchdog extends FileWatchdog {
public XMLWatchdog(String filename) {
super(filename);
}
public void doOnChange() {
new DOMConfigurator().doConfigure(filename, LogManager.getLoggerRepository());
}
}
其次,对XMLWatchdog的调用,new出对象实例,设置属性,然后调用start方法。如下所示:
DOMConfigurator.configureAndWatch(fileName, delay){
XMLWatchdog xdog = new XMLWatchdog(filename);
xdog.setDelay(delay);
xdog.start();
}
2. JDK中的FilenameFilter
在JDK的File类中,有一个list(FilenameFilter filter) 方法,实现的功能是根据filter定义的规则,获取并过滤该File对象对应的文件夹下面符合规则的文件和子文件夹的名字。
这里FilenameFilter是一个小接口:
interface FilenameFilter{
boolean accept(File dir, String name);
}
接口中accept方法定义了过滤的规则。
从设计的角度看,Filename的过滤功能和File本身没有太紧密的关系,过滤规则本身也是比较独立的、可变、易变的。所以JDK一方面把这部分设计成抽象的(因为可变、易变),另一方面把它设计成一个小接口(与File类关系不大,比较独立)。同时list方法定义FilenameFilter接口作为参数,采用的是面向抽象的、参数注入方法。
2.1 Class Structure
2.2 File.list()方法
pulbic Strring[] list(FilenameFilter filter)
{
String names[] = list();
if ((names == null) || (filter == null)) {
return names;
}
ArrayList v = new ArrayList();
for (int i = 0 ; i < names.length ; i++) {
if (filter.accept(this, names[i])) {
v.add(names[i]);
}
}
return (String[])(v.toArray(new String[0]));
}
3. Spring中的RowMapper
在Spring Framework源码中,通过JdbcTemplate.query(String sql, RowMapper rowMapper)方法,对SQL的执行结果resultset进行处理,返回相应的List结果。本质上RowMapper的职责是把resultset变成用户期望的ValueObject对象List。
3.1 Class Diagram
3.2 Call Work Flow
JdbcTemplate.query(String sql, RowMapper mapper)方法注入RowMapper接口。执行逻辑为:
- 注入RowMapper参数,封装出一个ResultSetExtractor实例
- 调用ResultSetExtractor.extractData(…)方法
- ResultSetExtractor.extractData(…)方法循环处理resultSet集合,对每一个结果行,调用注入的RowMapper对象的mapRow(…)方法。最终返回一个List对象
- JdbcTemplate返回一个Object对象(这里本质上是一个List对象)。
由上可见,JdbcTemplate.query()方法,执行SQL语句,最终结果经ResultSetExtractor接口,调用用户传入的RowMapper实例进行格式转换,返回List对象。
注意,ResultSetExtractor接口有多个实现子类,如SqlRowSetResultSetExtractor,RowMapperResultSetExtractor,RowCallbackHandlerResultSetExtractor等。主要是extractData(ResultSet)方法实现逻辑不同。
对于RowMapperResultSetExtractor,需要通过构造函数注入一个RowMapper实例,负责实际每行结果集的map工作。
整个过程体现了面向对象编程、封装“可变易变部分”的思路。用户只需扩展出自己的RowMapper实例即可。因为所有Exception处理都在JdbcTemplate中有所处理,所以用户在自己的RowMapper实现中不需要关心Exception处理。
4. 小结
通过学习优秀的开源代码,逐渐体会并掌握系统设计的一些基本原则,应该是一个很好的“临摹”过程,对自己的设计水平会有比较大的提高。这个方法应该是一个快速入门的捷径。