为了满足“开闭原则”,大部分设计模式都引入了抽象层,如工厂方法模式、抽象工厂模式、适配器模式、桥接模式、命令模式、策略模式等等。客户端代码针对抽象层编程,而在程序运行的时候再指定其子类,根据“里氏代换原则”和面向对象的多态性,子类对象在运行时将覆盖父类对象。如果需要对系统进行扩展或修改,只需修改子类类名即可。在具体实现时,通过引入配置文件可以使得用户在不修改任何客户端代码的前提下增加或替换子类,其基本实现过程如下:
(1)客户端针对抽象层编程,客户端代码中不能出现任何具体类类名,即客户端不直接实例化对象;
(2)引入纯文本格式的配置文件,通常是XML文件,将具体类类名存储在配置文件中;
(3)通过DOM(Document Object Model,文档对象模型)、SAX(Simple APIfor XML)等XML解析技术获取存储在配置文件中的类名;
(4)在客户端代码中通过反射(Reflection)机制根据类名创建对象,用反射所创建的对象替换父类对象的引用,程序运行时,将调用子类方法来实现业务功能;
(5)如果需要扩展功能,只需增加一个新的子类继承抽象父类,再修改配置文件,重新运行程序即可;如果需要替换功能,只需用另一个子类类名替换存储在配置文件中的原有子类类名即可。无论是扩展还是替换都无须修改既有类库和客户端源代码,完全符合开闭原则。
下面通过工厂方法模式来说明如何使用配置文件和反射机制:
【实例说明】:宝马(BMW)工厂可以生产宝马轿车,奔驰(Benz)工厂可以生产奔驰轿车,使用工厂方法模式来设计该场景,所得类图如图1所示:
图1 工厂方法模式实例类图
在图1中,CarFactory是抽象工厂,声明了工厂方法produceCar(),在其子类中实现了该方法,用于返回具体的产品。在客户端代码中将出现如下代码:
CarFactory cf; Car car; cf = new BMWFactory(); //创建具体工厂 car = cf.produceCar(); //使用工厂方法创建产品对象 car.run(); //调用产品的业务方法 |
在上述代码中,客户端针对抽象层编程,但是在创建具体工厂的时候还是涉及到了具体工厂子类类名,注意加粗的代码行。如果需要更换产品,如将BMW改为Benz,则需要更换工厂,要将BMWFactory改为BenzFactory,这将导致客户端代码发生修改。从客户端的角度而言违反了开闭原则,因此需要对上述代码进行改进。引入配置文件和反射机制是最佳的改进方法之一。
首先,我们将具体工厂类类名存储在如下XML文档中:
<?xml version="1.0"?> <config> <className>BMWFactory</className> </config> |
该XML文档即为配置文件,用于存储具体类的类名。Spring等主流的业务层框架都使用了XML格式的配置文件。
为了动态创建子类对象,我们需要再设计一个工具类XMLUtil用于读取该XML配置文件,在此使用Java语言实现该工具类。在XMLUtil的设计中需要使用Java语言的两个技术点,其一是DOM,即对XML文件的操作,关于DOM的详细学习可以参考其他相关书籍和资料,在此不予扩展;其二是Java反射机制,下面对Java反射机制做一个简单的介绍。
Java反射(Java Reflection)是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、父类等信息,还包 括实例的创建和实例类型的判断等。在反射中使用最多的类是Class,Class类的实例表示正在运行的Java应用程序中的类和接口,其forName(StringclassName)方法可以返回与带有给定字符串名的类或接口相关联的 Class对象,再通过Class对象的newInstance()方法创建此对象所表示的类的一个新实例,即通过一个类名字符串得到类的实例。如创建一个字符串类型的对象,其代码如下所示:
//通过类名生成实例对象并将其返回 Class c=Class.forName("String"); Object obj=c.newInstance(); return obj; |
此外,在JDK中还提供了java.lang.reflect包,封装了一些其他与反射相关的类,在本书中只用到上述简单的反射代码,在此不予扩展。
通过引入DOM和反射机制后,可以在XMLUtil中实现读取XML文件并根据存储在XML文件中的类名创建对应的对象,XMLUtil类的详细代码如下:
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()方法进行对象的实例化,代码修改如下:
CarFactory cf; Car car; cf = (CarFactory)XMLUtil.getBean();//getBean()的返回类型为Object,此处需要进行强制类型转换 car = cf.produceCar(); car.run(); |
在C#中实现读取配置文件和反射更为简单,我们只需先增加一个XML格式的配置文件,如App.config,代码如下所示:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="factory" value="Demo.CarFactory"/> </appSettings> </configuration> |
在.NET中反射生成对象也很简单,由于在.NET的程序集中封装了类型元数据信息,因此可以先通过Assembly的Load("程序集名称")方法加载一个程序集,再通过其CreateInstance("命名空间.类")方法根据类名创建一个object类型的对象,用户可以根据需要转换为所需类型。示意代码如下:
//导入命名空间 using System.Reflection; object obj = Assembly.Load("程序集名称").CreateInstance("命名空间.类"); |
在上述代码中,“命名空间.类”可以存储在配置文件中,使用ConfigurationManager类的AppSettings属性可以获取存储在配置文件中的类名字符串。客户端代码如下所示:
CarFactory cf; Car car; //读取配置文件 string factoryStr = ConfigurationManager.AppSettings["factory"]; //反射生成对象,程序集名为Demo cf = (CarFactory)Assembly.Load("Demo").CreateInstance(factoryStr); car = cf.ProduceCar(); car.Run(); |
由于C++语言的特性,在C++中实现类似Java或C#来反射生成对象的过程相对较为复杂,感兴趣的读者可以参考其他相关资料,在此不予扩展。
在引入配置文件和反射机制后,需要更换或增加新的具体类将变得很简单,只需增加新的具体类并修改配置文件即可,无须对现有类库和客户端代码进行任何修改,完全符合开闭原则。在很多设计模式中都可以通过引入配置文件和反射机制来对客户端代码进行改进,如在抽象工厂模式中可以将具体工厂类类名存储在配置文件中,在适配器模式中可以将适配器类类名存储在配置文件中,在策略模式中可以将具体策略类类名存储在配置文件中等等。通过对代码的改进,可以让系统具有更好的扩展性和灵活性,更加满足各种面向对象设计原则的要求
联系客服