工厂三兄弟之工厂方法模式(三)
完整解决方案
Sunny 公司开发人员决定使用工厂方法模式来设计日志记录器,其基本结构如图所示:
在图中,Logger 接口充当抽象产品,其子类 FileLogger 和 DatabaseLogger 充当具体产品,LoggerFactory 接口充当抽象工厂,其子类 FileLoggerFactory 和 DatabaseLoggerFactory 充当具体工厂。完整代码如下所示:
//日志记录器接口:抽象产品
interface Logger {
public void writeLog();
}
//数据库日志记录器:具体产品
class DatabaseLogger implements Logger {
public void writeLog() {
System.out.println("数据库日志记录。");
}
}
//文件日志记录器:具体产品
class FileLogger implements Logger {
public void writeLog() {
System.out.println("文件日志记录。");
}
}
//日志记录器工厂接口:抽象工厂
interface LoggerFactory {
public Logger createLogger();
}
//数据库日志记录器工厂类:具体工厂
class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//连接数据库,代码省略
//创建数据库日志记录器对象
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
}
//文件日志记录器工厂类:具体工厂
class FileLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//创建文件日志记录器对象
Logger logger = new FileLogger();
//创建文件,代码省略
return logger;
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
LoggerFactory factory;
Logger logger;
factory = new FileLoggerFactory(); //可引入配置文件实现
logger = factory.createLogger();
logger.writeLog();
}
}
编译并运行程序,输出结果如下:
文件日志记录。
反射与配置文件
为了让系统具有更好的灵活性和可扩展性,Sunny 公司开发人员决定对日志记录器客户端代码进行重构,使得可以在不修改任何客户端代码的基础上更换或增加新的日志记录方式。
在客户端代码中将不再使用 new 关键字来创建工厂对象,而是将具体工厂类的类名存储在配置文件(如 XML 文件)中,通过读取配置文件获取类名字符串,再使用Java的反射机制,根据类名字符串生成对象。在整个实现过程中需要用到两个技术:Java 反射机制与配置文件读取。软件系统的配置文件通常为XML文件,我们可以使用 DOM (Document Object Model)、SAX (Simple API for XML)、StAX (Streaming API for XML)等技术来处理 XML文件。关于 DOM、SAX、StAX 等技术的详细学习大家可以参考其他相关资料,在此不予扩展。
扩展
关于 Java 与 XML 的相关资料,大家可以阅读 Tom Myers 和 Alexander Nakhimovsky所著的《Java XML编程指南》一书或访问 developer Works 中国中的“Java XML 技术专题”。
Java 反射(Java Reflection)是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、父类等信息,还包括实例的创建和实例类型的判断等。在反射中使用最多的类是 Class,Class 类的实例表示正在运行的 Java 应用程序中的类和接口,其 forName(String className)方法可以返回与带有给定字符串名的类或接口相关联的 Class对象,再通过 Class 对象的 newInstance() 方法创建此对象所表示的类的一个新实例,即通过一个类名字符串得到类的实例。如创建一个字符串类型的对象,其代码如下:
//通过类名生成实例对象并将其返回
Class c=Class.forName("String");
Object obj=c.newInstance();
return obj;
此外,在 JDK 中还提供了 java.lang.reflect 包,封装了其他与反射相关的类,此处只用到上述简单的反射代码,在此不予扩展。
Sunny 公司开发人员创建了如下XML格式的配置文件 config.xml 用于存储具体日志记录器工厂类类名:
<!— config.xml -->
<?xml version="1.0"?>
<config>
<className>FileLoggerFactory</className>
</config>
为了读取该配置文件并通过存储在其中的类名字符串反射生成对象,Sunny 公司开发人员开发了一个名为 XMLUtil 的工具类,其详细代码如下所示:
//工具类XMLUtil.java
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
public class XMLUtil {
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
public static Object getBean() {
try {
//创建DOM文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));
//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();
//通过类名生成实例对象并将其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
}
有了 XMLUtil 类后,可以对日志记录器的客户端代码进行修改,不再直接使用 new 关键字来创建具体的工厂类,而是将具体工厂类的类名存储在 XML 文件中,再通过 XMLUtil 类的静态工厂方法 getBean() 方法进行对象的实例化,代码修改如下:
class Client {
public static void main(String args[]) {
LoggerFactory factory;
Logger logger;
factory = (LoggerFactory)XMLUtil.getBean(); //getBean()的返回类型为Object,需要进行强制类型转换
logger = factory.createLogger();
logger.writeLog();
}
}
引入 XMLUtil 类和 XML 配置文件后,如果要增加新的日志记录方式,只需要执行如下几个步骤:
(1) 新的日志记录器需要继承抽象日志记录器 Logger;
(2) 对应增加一个新的具体日志记录器工厂,继承抽象日志记录器工厂 LoggerFactory,并实现其中的工厂方法 createLogger(),设置好初始化参数和环境变量,返回具体日志记录器对象;
(3) 修改配置文件 config.xml,将新增的具体日志记录器工厂类的类名字符串替换原有工厂类类名字符串;
(4) 编译新增的具体日志记录器类和具体日志记录器工厂类,运行客户端测试类即可使用新的日志记录方式,而原有类库代码无须做任何修改,完全符合“开闭原则”。
通过上述重构可以使得系统更加灵活,由于很多设计模式都关注系统的可扩展性和灵活性,因此都定义了抽象层,在抽象层中声明业务方法,而将业务方法的实现放在实现层中。
思考
有人说:可以在客户端代码中直接通过反射机制来生成产品对象,在定义产品对象时使用抽象类型,同样可以确保系统的灵活性和可扩展性,增加新的具体产品类无须修改源代码,只需要将其作为抽象产品类的子类再修改配置文件即可,根本不需要抽象工厂类和具体工厂类。
试思考这种做法的可行性?如果可行,这种做法是否存在问题?为什么?